diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..1fa5455d --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.12-bookworm + +# Install Node.js 20.x +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* + +# Install global npm packages +RUN npm install -g husky vite + +# Create and activate Python virtual environment +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +WORKDIR /workspace \ No newline at end of file diff --git a/.devcontainer/devc-welcome.md b/.devcontainer/devc-welcome.md new file mode 100644 index 00000000..a119c590 --- /dev/null +++ b/.devcontainer/devc-welcome.md @@ -0,0 +1,49 @@ +# Welcome to DocsGPT Devcontainer + +Welcome to the DocsGPT development environment! This guide will help you get started quickly. + +## Starting Services + +To run DocsGPT, you need to start three main services: Flask (backend), Celery (task queue), and Vite (frontend). Here are the commands to start each service within the devcontainer: + +### Vite (Frontend) + +```bash +cd frontend +npm run dev -- --host +``` + +### Flask (Backend) + +```bash +flask --app application/app.py run --host=0.0.0.0 --port=7091 +``` + +### Celery (Task Queue) + +```bash +celery -A application.app.celery worker -l INFO +``` + +## Github Codespaces Instructions + +### 1. Make Ports Public: + +Go to the "Ports" panel in Codespaces (usually located at the bottom of the VS Code window). + +For both port 5173 and 7091, right-click on the port and select "Make Public". + +![CleanShot 2025-02-12 at 09 46 14@2x](https://github.com/user-attachments/assets/00a34b16-a7ef-47af-9648-87a7e3008475) + + + ### 2. Update VITE_API_HOST: + +After making port 7091 public, copy the public URL provided by Codespaces for port 7091. + +Open the file frontend/.env.development. + +Find the line VITE_API_HOST=http://localhost:7091. + +Replace http://localhost:7091 with the public URL you copied from Codespaces. + +![CleanShot 2025-02-12 at 09 46 56@2x](https://github.com/user-attachments/assets/c472242f-1079-4cd8-bc0b-2d78db22b94c) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..16188e32 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,24 @@ +{ + "name": "DocsGPT Dev Container", + "dockerComposeFile": ["docker-compose-dev.yaml", "docker-compose.override.yaml"], + "service": "dev", + "workspaceFolder": "/workspace", + "postCreateCommand": ".devcontainer/post-create-command.sh", + "forwardPorts": [7091, 5173, 6379, 27017], + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-toolsai.jupyter", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint" + ] + }, + "codespaces": { + "openFiles": [ + ".devcontainer/devc-welcome.md", + "CONTRIBUTING.md" + ] + } + } + } \ No newline at end of file diff --git a/docker-compose-dev.yaml b/.devcontainer/docker-compose-dev.yaml similarity index 100% rename from docker-compose-dev.yaml rename to .devcontainer/docker-compose-dev.yaml diff --git a/.devcontainer/docker-compose.override.yaml b/.devcontainer/docker-compose.override.yaml new file mode 100644 index 00000000..979bf48f --- /dev/null +++ b/.devcontainer/docker-compose.override.yaml @@ -0,0 +1,40 @@ +version: '3.8' + +services: + dev: + build: + context: . + dockerfile: Dockerfile + volumes: + - ../:/workspace:cached + command: sleep infinity + depends_on: + redis: + condition: service_healthy + mongo: + condition: service_healthy + environment: + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/1 + - MONGO_URI=mongodb://mongo:27017/docsgpt + - CACHE_REDIS_URL=redis://redis:6379/2 + networks: + - default + + redis: + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 30s + retries: 5 + + mongo: + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 5s + timeout: 30s + retries: 5 + +networks: + default: + name: docsgpt-dev-network \ No newline at end of file diff --git a/.devcontainer/post-create-command.sh b/.devcontainer/post-create-command.sh new file mode 100755 index 00000000..597b985e --- /dev/null +++ b/.devcontainer/post-create-command.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -e # Exit immediately if a command exits with a non-zero status + +if [ ! -f frontend/.env.development ]; then + cp -n .env-template frontend/.env.development || true # Assuming .env-template is in the root +fi + +# Determine VITE_API_HOST based on environment +if [ -n "$CODESPACES" ]; then + # Running in Codespaces + CODESPACE_NAME=$(echo "$CODESPACES" | cut -d'-' -f1) # Extract codespace name + PUBLIC_API_HOST="https://${CODESPACE_NAME}-7091.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" + echo "Setting VITE_API_HOST for Codespaces: $PUBLIC_API_HOST in frontend/.env.development" + sed -i "s|VITE_API_HOST=.*|VITE_API_HOST=$PUBLIC_API_HOST|" frontend/.env.development +else + # Not running in Codespaces (local devcontainer) + DEFAULT_API_HOST="http://localhost:7091" + echo "Setting VITE_API_HOST for local dev: $DEFAULT_API_HOST in frontend/.env.development" + sed -i "s|VITE_API_HOST=.*|VITE_API_HOST=$DEFAULT_API_HOST|" frontend/.env.development +fi + + +mkdir -p model +if [ ! -d model/all-mpnet-base-v2 ]; then + wget -q https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip -O model/mpnet-base-v2.zip + unzip -q model/mpnet-base-v2.zip -d model + rm model/mpnet-base-v2.zip +fi +pip install -r application/requirements.txt +cd frontend +npm install --include=dev \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..d01386f2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: arc53 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dd0799c6..a41a1f3e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,12 +8,12 @@ updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/application" # Location of package manifests schedule: - interval: "weekly" + interval: "daily" - package-ecosystem: "npm" # See documentation for possible values directory: "/frontend" # Location of package manifests schedule: - interval: "weekly" + interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "daily" diff --git a/.github/workflows/bandit.yaml b/.github/workflows/bandit.yaml new file mode 100644 index 00000000..db5f3fc8 --- /dev/null +++ b/.github/workflows/bandit.yaml @@ -0,0 +1,40 @@ +name: Bandit Security Scan + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +jobs: + bandit_scan: + if: ${{ github.repository == 'arc53/DocsGPT' }} + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install bandit # Bandit is needed for this action + if [ -f application/requirements.txt ]; then pip install -r application/requirements.txt; fi + + - name: Run Bandit scan + uses: PyCQA/bandit-action@v1 + with: + severity: medium + confidence: medium + targets: application/ + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 090a69db..67ebe0dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,20 +5,33 @@ on: types: [published] jobs: - deploy: + build: if: github.repository == 'arc53/DocsGPT' - runs-on: ubuntu-latest + strategy: + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + suffix: amd64 + - platform: linux/arm64 + runner: ubuntu-24.04-arm + suffix: arm64 + runs-on: ${{ matrix.runner }} permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - - name: Set up QEMU + - name: Set up QEMU # Only needed for emulation, not for native arm64 builds + if: matrix.platform == 'linux/arm64' uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + install: true - name: Login to DockerHub uses: docker/login-action@v3 @@ -33,15 +46,67 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker images to docker.io and ghcr.io + - name: Build and push platform-specific images uses: docker/build-push-action@v6 with: file: './application/Dockerfile' - platforms: linux/amd64 + platforms: ${{ matrix.platform }} context: ./application push: true tags: | - ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }},${{ secrets.DOCKER_USERNAME }}/docsgpt:latest - ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }},ghcr.io/${{ github.repository_owner }}/docsgpt:latest + ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-${{ matrix.suffix }} + ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-${{ matrix.suffix }} + provenance: false + sbom: false cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:latest cache-to: type=inline + + manifest: + if: github.repository == 'arc53/DocsGPT' + needs: build + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + install: true + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push manifest for DockerHub + run: | + set -e + docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }} \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-arm64 + docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }} + docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt:latest \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-arm64 + docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt:latest + + - name: Create and push manifest for ghcr.io + run: | + set -e + docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }} \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-arm64 + docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }} + docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt:latest \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-arm64 + docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt:latest \ No newline at end of file diff --git a/.github/workflows/cife.yml b/.github/workflows/cife.yml index 711cab29..1118723f 100644 --- a/.github/workflows/cife.yml +++ b/.github/workflows/cife.yml @@ -5,20 +5,33 @@ on: types: [published] jobs: - deploy: + build: if: github.repository == 'arc53/DocsGPT' - runs-on: ubuntu-latest + strategy: + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + suffix: amd64 + - platform: linux/arm64 + runner: ubuntu-24.04-arm + suffix: arm64 + runs-on: ${{ matrix.runner }} permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - - name: Set up QEMU + - name: Set up QEMU # Only needed for emulation, not for native arm64 builds + if: matrix.platform == 'linux/arm64' uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + install: true - name: Login to DockerHub uses: docker/login-action@v3 @@ -33,16 +46,67 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - # Runs a single command using the runners shell - - name: Build and push Docker images to docker.io and ghcr.io + - name: Build and push platform-specific images uses: docker/build-push-action@v6 with: file: './frontend/Dockerfile' - platforms: linux/amd64, linux/arm64 + platforms: ${{ matrix.platform }} context: ./frontend push: true tags: | - ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }},${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest - ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }},ghcr.io/${{ github.repository_owner }}/docsgpt-fe:latest + ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }}-${{ matrix.suffix }} + ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }}-${{ matrix.suffix }} + provenance: false + sbom: false cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest cache-to: type=inline + + manifest: + if: github.repository == 'arc53/DocsGPT' + needs: build + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + install: true + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push manifest for DockerHub + run: | + set -e + docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }} \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }}-amd64 \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }}-arm64 + docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }} + docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }}-amd64 \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }}-arm64 + docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest + + - name: Create and push manifest for ghcr.io + run: | + set -e + docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }} \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }}-amd64 \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }}-arm64 + docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }} + docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt-fe:latest \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }}-amd64 \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }}-arm64 + docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt-fe:latest \ No newline at end of file diff --git a/.github/workflows/docker-develop-build.yml b/.github/workflows/docker-develop-build.yml index 5a22b1a1..44a61769 100644 --- a/.github/workflows/docker-develop-build.yml +++ b/.github/workflows/docker-develop-build.yml @@ -1,4 +1,4 @@ -name: Build and push DocsGPT Docker image for development +name: Build and push multi-arch DocsGPT Docker image on: workflow_dispatch: @@ -7,27 +7,36 @@ on: - main jobs: - deploy: + build: if: github.repository == 'arc53/DocsGPT' - runs-on: ubuntu-latest + strategy: + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + suffix: amd64 + - platform: linux/arm64 + runner: ubuntu-24.04-arm + suffix: arm64 + runs-on: ${{ matrix.runner }} permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + install: true - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - + - name: Login to ghcr.io uses: docker/login-action@v3 with: @@ -35,15 +44,57 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker images to docker.io and ghcr.io + - name: Build and push platform-specific images uses: docker/build-push-action@v6 with: file: './application/Dockerfile' - platforms: linux/amd64 + platforms: ${{ matrix.platform }} context: ./application push: true tags: | - ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop - ghcr.io/${{ github.repository_owner }}/docsgpt:develop + ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop-${{ matrix.suffix }} + ghcr.io/${{ github.repository_owner }}/docsgpt:develop-${{ matrix.suffix }} + provenance: false + sbom: false cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:develop cache-to: type=inline + + manifest: + if: github.repository == 'arc53/DocsGPT' + needs: build + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + install: true + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push manifest for DockerHub + run: | + docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop-amd64 \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop-arm64 + docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop + + - name: Create and push manifest for ghcr.io + run: | + docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt:develop \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt:develop-amd64 \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt:develop-arm64 + docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt:develop \ No newline at end of file diff --git a/.github/workflows/docker-develop-fe-build.yml b/.github/workflows/docker-develop-fe-build.yml index 317635bd..df7bdac6 100644 --- a/.github/workflows/docker-develop-fe-build.yml +++ b/.github/workflows/docker-develop-fe-build.yml @@ -7,20 +7,33 @@ on: - main jobs: - deploy: + build: if: github.repository == 'arc53/DocsGPT' - runs-on: ubuntu-latest + strategy: + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + suffix: amd64 + - platform: linux/arm64 + runner: ubuntu-24.04-arm + suffix: arm64 + runs-on: ${{ matrix.runner }} permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - - name: Set up QEMU + - name: Set up QEMU # Only needed for emulation, not for native arm64 builds + if: matrix.platform == 'linux/arm64' uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + install: true - name: Login to DockerHub uses: docker/login-action@v3 @@ -35,15 +48,57 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker images to docker.io and ghcr.io + - name: Build and push platform-specific images uses: docker/build-push-action@v6 with: file: './frontend/Dockerfile' - platforms: linux/amd64 + platforms: ${{ matrix.platform }} context: ./frontend push: true tags: | - ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop - ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop + ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop-${{ matrix.suffix }} + ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop-${{ matrix.suffix }} + provenance: false + sbom: false cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop cache-to: type=inline + + manifest: + if: github.repository == 'arc53/DocsGPT' + needs: build + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + install: true + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push manifest for DockerHub + run: | + docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop-amd64 \ + --amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop-arm64 + docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop + + - name: Create and push manifest for ghcr.io + run: | + docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop-amd64 \ + --amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop-arm64 + docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop \ No newline at end of file diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index b858a0f7..d5b31109 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.11"] + python-version: ["3.12"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -23,8 +23,8 @@ jobs: run: | python -m pytest --cov=application --cov-report=xml - name: Upload coverage reports to Codecov - if: github.event_name == 'pull_request' && matrix.python-version == '3.11' - uses: codecov/codecov-action@v4 + if: github.event_name == 'pull_request' && matrix.python-version == '3.12' + uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.vscode/launch.json b/.vscode/launch.json index fc4b8128..5083d977 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,44 @@ "skipFiles": [ "/**" ] + }, + { + "name": "Flask Debugger", + "type": "debugpy", + "request": "launch", + "module": "flask", + "env": { + "FLASK_APP": "application/app.py", + "PYTHONPATH": "${workspaceFolder}", + "FLASK_ENV": "development", + "FLASK_DEBUG": "1", + "FLASK_RUN_PORT": "7091", + "FLASK_RUN_HOST": "0.0.0.0" + + }, + "args": [ + "run", + "--no-debugger" + ], + "cwd": "${workspaceFolder}", + }, + { + "name": "Celery Debugger", + "type": "debugpy", + "request": "launch", + "module": "celery", + "env": { + "PYTHONPATH": "${workspaceFolder}", + }, + "args": [ + "-A", + "application.app.celery", + "worker", + "-l", + "INFO", + "--pool=solo" + ], + "cwd": "${workspaceFolder}" } ] } \ No newline at end of file diff --git a/Assets/DocsGPT tee-back.jpeg b/Assets/DocsGPT tee-back.jpeg deleted file mode 100644 index 8c0e22aa..00000000 Binary files a/Assets/DocsGPT tee-back.jpeg and /dev/null differ diff --git a/Assets/DocsGPT tee-front.jpeg b/Assets/DocsGPT tee-front.jpeg deleted file mode 100644 index 8a4b7374..00000000 Binary files a/Assets/DocsGPT tee-front.jpeg and /dev/null differ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b0567e4..096f236b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,7 @@ Before creating issues, please check out how the latest version of our app looks ### 👨‍💻 If you're interested in contributing code, here are some important things to know: +For instructions on setting up a development environment, please refer to our [Development Deployment Guide](https://docs.docsgpt.cloud/Deploying/Development-Environment). Tech Stack Overview: @@ -34,19 +35,40 @@ Tech Stack Overview: - 🖥 Backend: Developed in Python 🐍 -### 🌐 If you are looking to contribute to frontend (⚛️React, Vite): +### 🌐 Frontend Contributions (⚛️ React, Vite) -- The current frontend is being migrated from [`/application`](https://github.com/arc53/DocsGPT/tree/main/application) to [`/frontend`](https://github.com/arc53/DocsGPT/tree/main/frontend) with a new design, so please contribute to the new one. -- Check out this [milestone](https://github.com/arc53/DocsGPT/milestone/1) and its issues. -- The updated Figma design can be found [here](https://www.figma.com/file/OXLtrl1EAy885to6S69554/DocsGPT?node-id=0%3A1&t=hjWVuxRg9yi5YkJ9-1). +* The updated Figma design can be found [here](https://www.figma.com/file/OXLtrl1EAy885to6S69554/DocsGPT?node-id=0%3A1&t=hjWVuxRg9yi5YkJ9-1). Please try to follow the guidelines. +* **Coding Style:** We follow a strict coding style enforced by ESLint and Prettier. Please ensure your code adheres to the configuration provided in our repository's `fronetend/.eslintrc.js` file. We recommend configuring your editor with ESLint and Prettier to help with this. +* **Component Structure:** Strive for small, reusable components. Favor functional components and hooks over class components where possible. +* **State Management** If you need to add stores, please use Redux. -Please try to follow the guidelines. +### 🖥 Backend Contributions (🐍 Python) -### 🖥 If you are looking to contribute to Backend (🐍 Python): - -- Review our issues and contribute to [`/application`](https://github.com/arc53/DocsGPT/tree/main/application) or [`/scripts`](https://github.com/arc53/DocsGPT/tree/main/scripts) (please disregard old [`ingest_rst.py`](https://github.com/arc53/DocsGPT/blob/main/scripts/old/ingest_rst.py) [`ingest_rst_sphinx.py`](https://github.com/arc53/DocsGPT/blob/main/scripts/old/ingest_rst_sphinx.py) files; these will be deprecated soon). +- Review our issues and contribute to [`/application`](https://github.com/arc53/DocsGPT/tree/main/application) - All new code should be covered with unit tests ([pytest](https://github.com/pytest-dev/pytest)). Please find tests under [`/tests`](https://github.com/arc53/DocsGPT/tree/main/tests) folder. - Before submitting your Pull Request, ensure it can be queried after ingesting some test data. +- **Coding Style:** We adhere to the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide for Python code. We use `ruff` as our linter and code formatter. Please ensure your code is formatted correctly and passes `ruff` checks before submitting. +- **Type Hinting:** Please use type hints for all function arguments and return values. This improves code readability and helps catch errors early. Example: + + ```python + def my_function(name: str, count: int) -> list[str]: + ... + ``` +- **Docstrings:** All functions and classes should have docstrings explaining their purpose, parameters, and return values. We prefer the [Google style docstrings](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). Example: + + ```python + def my_function(name: str, count: int) -> list[str]: + """Does something with a name and a count. + + Args: + name: The name to use. + count: The number of times to do it. + + Returns: + A list of strings. + """ + ... + ``` ### Testing diff --git a/HACKTOBERFEST.md b/HACKTOBERFEST.md deleted file mode 100644 index 8656bd84..00000000 --- a/HACKTOBERFEST.md +++ /dev/null @@ -1,41 +0,0 @@ -# **🎉 Join the Hacktoberfest with DocsGPT and win a Free T-shirt and other prizes! 🎉** - -Welcome, contributors! We're excited to announce that DocsGPT is participating in Hacktoberfest. Get involved by submitting meaningful pull requests. - -All contributors with accepted PRs will receive a cool Holopin! 🤩 (Watch out for a reply in your PR to collect it). - -### 🏆 Top 50 contributors will receive a special T-shirt - -### 🏆 [LLM Document analysis by LexEU competition](https://github.com/arc53/DocsGPT/blob/main/lexeu-competition.md): -A separate competition is available for those who submit new retrieval / workflow method that will analyze a Document using EU laws. -With 200$, 100$, 50$ prize for 1st, 2nd and 3rd place respectively. -You can find more information [here](https://github.com/arc53/DocsGPT/blob/main/lexeu-competition.md) - -## 📜 Here's How to Contribute: -```text -🛠️ Code: This is the golden ticket! Make meaningful contributions through PRs. - -🧩 API extension: Build an app utilising DocsGPT API. We prefer submissions that showcase original ideas and turn the API into an AI agent. -They can be a completely separate repos. -For example: -https://github.com/arc53/tg-bot-docsgpt-extenstion or -https://github.com/arc53/DocsGPT-cli - -Non-Code Contributions: - -📚 Wiki: Improve our documentation, create a guide or change existing documentation. - -🖥️ Design: Improve the UI/UX or design a new feature. - -📝 Blogging or Content Creation: Write articles or create videos to showcase DocsGPT or highlight your contributions! -``` - -### 📝 Guidelines for Pull Requests: -- Familiarize yourself with the current contributions and our [Roadmap](https://github.com/orgs/arc53/projects/2). -- Before contributing we highly advise that you check existing [issues](https://github.com/arc53/DocsGPT/issues) or [create](https://github.com/arc53/DocsGPT/issues/new/choose) an issue and wait to get assigned. -- Once you are finished with your contribution, please fill in this [form](https://airtable.com/appikMaJwdHhC1SDP/pagoblCJ9W29wf6Hf/form). -- Refer to the [Documentation](https://docs.docsgpt.cloud/). -- Feel free to join our [Discord](https://discord.gg/n5BX8dh8rU) server. We're here to help newcomers, so don't hesitate to jump in! Join us [here](https://discord.gg/n5BX8dh8rU). - -Thank you very much for considering contributing to DocsGPT during Hacktoberfest! 🙏 Your contributions (not just simple typos) could earn you a stylish new t-shirt and other prizes as a token of our appreciation. 🎁 Join us, and let's code together! 🚀 - diff --git a/README.md b/README.md index 99baf811..814b772a 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,11 @@

- Open-Source Documentation Assistant + Open-Source RAG Assistant

- DocsGPT is a cutting-edge open-source solution that streamlines the process of finding information in the project documentation. With its integration of the powerful GPT models, developers can easily ask questions about a project and receive accurate answers. - -Say goodbye to time-consuming manual searches, and let DocsGPT help you quickly find the information you need. Try it out and see how it revolutionizes your project documentation experience. Contribute to its development and be a part of the future of AI-powered assistance. + DocsGPT is an open-source genAI tool that helps users get reliable answers from any knowledge source, while avoiding hallucinations. It enables quick and reliable information retrieval, with tooling and agentic system capability built in.

@@ -17,180 +15,120 @@ Say goodbye to time-consuming manual searches, and let ![link to main GitHub showing Stars number](https://img.shields.io/github/stars/arc53/docsgpt?style=social) ![link to main GitHub showing Forks number](https://img.shields.io/github/forks/arc53/docsgpt?style=social) ![link to license file](https://img.shields.io/github/license/arc53/docsgpt) + ![link to discord](https://img.shields.io/discord/1070046503302877216) ![X (formerly Twitter) URL](https://img.shields.io/twitter/follow/docsgptai) - + ⚡️ Quickstart☁️ Cloud Version💬 Discord +
+ 📖 Documentation👫 Contribute🗞 Blog +
+
+
+video-example-of-docs-gpt +
+

+ Key Features: +

+ + +## Roadmap + +- [x] Full GoogleAI compatibility (Jan 2025) +- [x] Add tools (Jan 2025) +- [x] Manually updating chunks in the app UI (Feb 2025) +- [x] Devcontainer for easy development (Feb 2025) +- [ ] Anthropic Tool compatibility +- [ ] Add triggerable actions / tools (webhook) +- [ ] Add OAuth 2.0 authentication for tools and sources +- [ ] Chatbots menu re-design to handle tools, scheduling, and more + +You can find our full roadmap [here](https://github.com/orgs/arc53/projects/2). Please don't hesitate to contribute or create issues, it helps us improve DocsGPT! ### Production Support / Help for Companies: We're eager to provide personalized assistance when deploying your DocsGPT to a live environment. - - Let's chat - +[Get a Demo :wave:](https://www.docsgpt.cloud/contact)⁠ -[Send Email :email:](mailto:contact@arc53.com?subject=DocsGPT%20support%2Fsolutions) +[Send Email :email:](mailto:support@docsgpt.cloud?subject=DocsGPT%20support%2Fsolutions) -video-example-of-docs-gpt - -## Roadmap - -You can find our roadmap [here](https://github.com/orgs/arc53/projects/2). Please don't hesitate to contribute or create issues, it helps us improve DocsGPT! - -## Our Open-Source Models Optimized for DocsGPT: - -| Name | Base Model | Requirements (or similar) | -| --------------------------------------------------------------------- | ----------- | ------------------------- | -| [Docsgpt-7b-mistral](https://huggingface.co/Arc53/docsgpt-7b-mistral) | Mistral-7b | 1xA10G gpu | -| [Docsgpt-14b](https://huggingface.co/Arc53/docsgpt-14b) | llama-2-14b | 2xA10 gpu's | -| [Docsgpt-40b-falcon](https://huggingface.co/Arc53/docsgpt-40b-falcon) | falcon-40b | 8xA10G gpu's | - -If you don't have enough resources to run it, you can use bitsnbytes to quantize. - -## End to End AI Framework for Information Retrieval - -![Architecture chart](https://github.com/user-attachments/assets/fc6a7841-ddfc-45e6-b5a0-d05fe648cbe2) - -## Useful Links - -- :mag: :fire: [Cloud Version](https://app.docsgpt.cloud/) - -- :speech_balloon: :tada: [Join our Discord](https://discord.gg/n5BX8dh8rU) - -- :books: :sunglasses: [Guides](https://docs.docsgpt.cloud/) - -- :couple: [Interested in contributing?](https://github.com/arc53/DocsGPT/blob/main/CONTRIBUTING.md) - -- :file_folder: :rocket: [How to use any other documentation](https://docs.docsgpt.cloud/Guides/How-to-train-on-other-documentation) - -- :house: :closed_lock_with_key: [How to host it locally (so all data will stay on-premises)](https://docs.docsgpt.cloud/Guides/How-to-use-different-LLM) - -## Project Structure - -- Application - Flask app (main application). - -- Extensions - Chrome extension. - -- Scripts - Script that creates similarity search index for other libraries. - -- Frontend - Frontend uses Vite and React. - ## QuickStart > [!Note] > Make sure you have [Docker](https://docs.docker.com/engine/install/) installed -On Mac OS or Linux, write: +A more detailed [Quickstart](https://docs.docsgpt.cloud/quickstart) is available in our documentation -`./setup.sh` +1. **Clone the repository:** -It will install all the dependencies and allow you to download the local model, use OpenAI or use our LLM API. - -Otherwise, refer to this Guide for Windows: - -1. Download and open this repository with `git clone https://github.com/arc53/DocsGPT.git` -2. Create a `.env` file in your root directory and set the env variables and `VITE_API_STREAMING` to true or false, depending on whether you want streaming answers or not. - It should look like this inside: - - ``` - LLM_NAME=[docsgpt or openai or others] - VITE_API_STREAMING=true - API_KEY=[if LLM_NAME is openai] + ```bash + git clone https://github.com/arc53/DocsGPT.git + cd DocsGPT ``` - See optional environment variables in the [/.env-template](https://github.com/arc53/DocsGPT/blob/main/.env-template) and [/application/.env_sample](https://github.com/arc53/DocsGPT/blob/main/application/.env_sample) files. +**For macOS and Linux:** -3. Run [./run-with-docker-compose.sh](https://github.com/arc53/DocsGPT/blob/main/run-with-docker-compose.sh). -4. Navigate to http://localhost:5173/. +2. **Run the setup script:** -To stop, just run `Ctrl + C`. + ```bash + ./setup.sh + ``` -## Development Environments +This interactive script will guide you through setting up DocsGPT. It offers four options: using the public API, running locally, connecting to a local inference engine, or using a cloud API provider. The script will automatically configure your `.env` file and handle necessary downloads and installations based on your chosen option. -### Spin up Mongo and Redis +**For Windows:** -For development, only two containers are used from [docker-compose.yaml](https://github.com/arc53/DocsGPT/blob/main/docker-compose.yaml) (by deleting all services except for Redis and Mongo). -See file [docker-compose-dev.yaml](./docker-compose-dev.yaml). +2. **Follow the Docker Deployment Guide:** -Run + Please refer to the [Docker Deployment documentation](https://docs.docsgpt.cloud/Deploying/Docker-Deploying) for detailed step-by-step instructions on setting up DocsGPT using Docker. +**Navigate to http://localhost:5173/** + +To stop DocsGPT, open a terminal in the `DocsGPT` directory and run: + +```bash +docker compose -f deployment/docker-compose.yaml down ``` -docker compose -f docker-compose-dev.yaml build -docker compose -f docker-compose-dev.yaml up -d -``` - -### Run the Backend +(or use the specific `docker compose down` command shown after running `setup.sh`). > [!Note] -> Make sure you have Python 3.10 or 3.11 installed. - -1. Export required environment variables or prepare a `.env` file in the project folder: - - Copy [.env-template](https://github.com/arc53/DocsGPT/blob/main/application/.env-template) and create `.env`. - -(check out [`application/core/settings.py`](application/core/settings.py) if you want to see more config options.) - -2. (optional) Create a Python virtual environment: - You can follow the [Python official documentation](https://docs.python.org/3/tutorial/venv.html) for virtual environments. - -a) On Mac OS and Linux - -```commandline -python -m venv venv -. venv/bin/activate -``` - -b) On Windows - -```commandline -python -m venv venv - venv/Scripts/activate -``` - -3. Download embedding model and save it in the `model/` folder: -You can use the script below, or download it manually from [here](https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip), unzip it and save it in the `model/` folder. - -```commandline -wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip -unzip mpnet-base-v2.zip -d model -rm mpnet-base-v2.zip -``` - -4. Install dependencies for the backend: - -```commandline -pip install -r application/requirements.txt -``` - -5. Run the app using `flask --app application/app.py run --host=0.0.0.0 --port=7091`. -6. Start worker with `celery -A application.app.celery worker -l INFO`. - -### Start Frontend - -> [!Note] -> Make sure you have Node version 16 or higher. - -1. Navigate to the [/frontend](https://github.com/arc53/DocsGPT/tree/main/frontend) folder. -2. Install the required packages `husky` and `vite` (ignore if already installed). - -```commandline -npm install husky -g -npm install vite -g -``` - -3. Install dependencies by running `npm install --include=dev`. -4. Run the app using `npm run dev`. +> For development environment setup instructions, please refer to the [Development Environment Guide](https://docs.docsgpt.cloud/Deploying/Development-Environment). ## Contributing Please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file for information about how to get involved. We welcome issues, questions, and pull requests. +## Architecture + +![Architecture chart](https://github.com/user-attachments/assets/fc6a7841-ddfc-45e6-b5a0-d05fe648cbe2) + +## Project Structure + +- Application - Flask app (main application). + +- Extensions - Extensions, like react widget or discord bot. + +- Frontend - Frontend uses Vite and React. + +- Scripts - Miscellaneous scripts. + ## Code Of Conduct We as members, contributors, and leaders, pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. Please refer to the [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) file for more information about contributing. + ## Many Thanks To Our Contributors⚡ diff --git a/Readme Logo.png b/Readme Logo.png deleted file mode 100644 index aad92a75..00000000 Binary files a/Readme Logo.png and /dev/null differ diff --git a/application/Dockerfile b/application/Dockerfile index d076bc41..308b721b 100644 --- a/application/Dockerfile +++ b/application/Dockerfile @@ -6,21 +6,20 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y software-properties-common && \ add-apt-repository ppa:deadsnakes/ppa && \ -# Install necessary packages and Python apt-get update && \ - apt-get install -y --no-install-recommends gcc wget unzip libc6-dev python3.11 python3.11-distutils python3.11-venv && \ + apt-get install -y --no-install-recommends gcc wget unzip libc6-dev python3.12 python3.12-venv && \ rm -rf /var/lib/apt/lists/* # Verify Python installation and setup symlink -RUN if [ -f /usr/bin/python3.11 ]; then \ - ln -s /usr/bin/python3.11 /usr/bin/python; \ +RUN if [ -f /usr/bin/python3.12 ]; then \ + ln -s /usr/bin/python3.12 /usr/bin/python; \ else \ - echo "Python 3.11 not found"; exit 1; \ + echo "Python 3.12 not found"; exit 1; \ fi # Download and unzip the model RUN wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip && \ - unzip mpnet-base-v2.zip -d model && \ + unzip mpnet-base-v2.zip -d models && \ rm mpnet-base-v2.zip # Install Rust @@ -33,7 +32,7 @@ RUN apt-get remove --purge -y wget unzip && apt-get autoremove -y && rm -rf /var COPY requirements.txt . # Setup Python virtual environment -RUN python3.11 -m venv /venv +RUN python3.12 -m venv /venv # Activate virtual environment and install Python packages ENV PATH="/venv/bin:$PATH" @@ -49,9 +48,8 @@ FROM ubuntu:24.04 as final RUN apt-get update && \ apt-get install -y software-properties-common && \ add-apt-repository ppa:deadsnakes/ppa && \ -# Install Python - apt-get update && apt-get install -y --no-install-recommends python3.11 && \ - ln -s /usr/bin/python3.11 /usr/bin/python && \ + apt-get update && apt-get install -y --no-install-recommends python3.12 && \ + ln -s /usr/bin/python3.12 /usr/bin/python && \ rm -rf /var/lib/apt/lists/* # Set working directory @@ -63,7 +61,8 @@ RUN groupadd -r appuser && \ # Copy the virtual environment and model from the builder stage COPY --from=builder /venv /venv -COPY --from=builder /model /app/model + +COPY --from=builder /models /app/models # Copy your application code COPY . /app/application diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index f109db26..34e6abca 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -1,14 +1,13 @@ import asyncio import datetime import json -import logging import os -import sys import traceback +import logging from bson.dbref import DBRef from bson.objectid import ObjectId -from flask import Blueprint, current_app, make_response, request, Response +from flask import Blueprint, make_response, request, Response from flask_restx import fields, Namespace, Resource @@ -18,7 +17,7 @@ from application.error import bad_request from application.extensions import api from application.llm.llm_creator import LLMCreator from application.retriever.retriever_creator import RetrieverCreator -from application.utils import check_required_fields +from application.utils import check_required_fields, limit_chat_history logger = logging.getLogger(__name__) @@ -37,7 +36,7 @@ api.add_namespace(answer_ns) gpt_model = "" # to have some kind of default behaviour if settings.LLM_NAME == "openai": - gpt_model = "gpt-3.5-turbo" + gpt_model = "gpt-4o-mini" elif settings.LLM_NAME == "anthropic": gpt_model = "claude-2" elif settings.LLM_NAME == "groq": @@ -89,9 +88,6 @@ def get_data_from_api_key(api_key): if data is None: raise Exception("Invalid API Key, please generate new key", 401) - if "retriever" not in data: - data["retriever"] = None - if "source" in data and isinstance(data["source"], DBRef): source_doc = db.dereference(data["source"]) data["source"] = str(source_doc["_id"]) @@ -118,8 +114,27 @@ def is_azure_configured(): ) -def save_conversation(conversation_id, question, response, source_log_docs, llm): - if conversation_id is not None and conversation_id != "None": +def save_conversation( + conversation_id, question, response, source_log_docs, tool_calls, llm, index=None +): + if conversation_id is not None and index is not None: + conversations_collection.update_one( + {"_id": ObjectId(conversation_id), f"queries.{index}": {"$exists": True}}, + { + "$set": { + f"queries.{index}.prompt": question, + f"queries.{index}.response": response, + f"queries.{index}.sources": source_log_docs, + f"queries.{index}.tool_calls": tool_calls, + } + }, + ) + ##remove following queries from the array + conversations_collection.update_one( + {"_id": ObjectId(conversation_id), f"queries.{index}": {"$exists": True}}, + {"$push": {"queries": {"$each": [], "$slice": index + 1}}}, + ) + elif conversation_id is not None and conversation_id != "None": conversations_collection.update_one( {"_id": ObjectId(conversation_id)}, { @@ -128,6 +143,7 @@ def save_conversation(conversation_id, question, response, source_log_docs, llm) "prompt": question, "response": response, "sources": source_log_docs, + "tool_calls": tool_calls, } } }, @@ -141,17 +157,13 @@ def save_conversation(conversation_id, question, response, source_log_docs, llm) "role": "assistant", "content": "Summarise following conversation in no more than 3 " "words, respond ONLY with the summary, use the same " - "language as the system \n\nUser: " - + question - + "\n\n" - + "AI: " - + response, + "language as the system", }, { "role": "user", "content": "Summarise following conversation in no more than 3 words, " "respond ONLY with the summary, use the same language as the " - "system", + "system \n\nUser: " + question + "\n\n" + "AI: " + response, }, ] @@ -166,6 +178,7 @@ def save_conversation(conversation_id, question, response, source_log_docs, llm) "prompt": question, "response": response, "sources": source_log_docs, + "tool_calls": tool_calls, } ], } @@ -186,12 +199,13 @@ def get_prompt(prompt_id): def complete_stream( - question, retriever, conversation_id, user_api_key, isNoneDoc=False + question, retriever, conversation_id, user_api_key, isNoneDoc=False, index=None ): try: response_full = "" source_log_docs = [] + tool_calls = [] answer = retriever.gen() sources = retriever.search() for source in sources: @@ -200,6 +214,7 @@ def complete_stream( if len(sources) > 0: data = json.dumps({"type": "source", "source": sources}) yield f"data: {data}\n\n" + for line in answer: if "answer" in line: response_full += str(line["answer"]) @@ -207,6 +222,10 @@ def complete_stream( yield f"data: {data}\n\n" elif "source" in line: source_log_docs.append(line["source"]) + elif "tool_calls" in line: + tool_calls = line["tool_calls"] + data = json.dumps({"type": "tool_calls", "tool_calls": tool_calls}) + yield f"data: {data}\n\n" if isNoneDoc: for doc in source_log_docs: @@ -217,7 +236,13 @@ def complete_stream( ) if user_api_key is None: conversation_id = save_conversation( - conversation_id, question, response_full, source_log_docs, llm + conversation_id, + question, + response_full, + source_log_docs, + tool_calls, + llm, + index, ) # send data.type = "end" to indicate that the stream has ended as json data = json.dumps({"type": "id", "id": str(conversation_id)}) @@ -240,13 +265,12 @@ def complete_stream( data = json.dumps({"type": "end"}) yield f"data: {data}\n\n" except Exception as e: - print("\033[91merr", str(e), file=sys.stderr) - traceback.print_exc() + logger.error(f"Error in stream: {str(e)}") + logger.error(traceback.format_exc()) data = json.dumps( { "type": "error", "error": "Please try again later. We apologize for any inconvenience.", - "error_exception": str(e), } ) yield f"data: {data}\n\n" @@ -282,6 +306,9 @@ class Stream(Resource): "isNoneDoc": fields.Boolean( required=False, description="Flag indicating if no document is used" ), + "index": fields.Integer( + required=False, description="The position where query is to be updated" + ), }, ) @@ -290,19 +317,21 @@ class Stream(Resource): def post(self): data = request.get_json() required_fields = ["question"] - + if "index" in data: + required_fields = ["question", "conversation_id"] missing_fields = check_required_fields(data, required_fields) if missing_fields: return missing_fields try: question = data["question"] - history = data.get("history", []) - history = json.loads(history) + history = limit_chat_history( + json.loads(data.get("history", [])), gpt_model=gpt_model + ) conversation_id = data.get("conversation_id") prompt_id = data.get("prompt_id", "default") - + index = data.get("index", None) chunks = int(data.get("chunks", 2)) token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY) retriever_name = data.get("retriever", "classic") @@ -324,7 +353,7 @@ class Stream(Resource): source = {} user_api_key = None - current_app.logger.info( + logger.info( f"/stream - request_data: {data}, source: {source}", extra={"data": json.dumps({"request_data": data, "source": source})}, ) @@ -351,30 +380,27 @@ class Stream(Resource): conversation_id=conversation_id, user_api_key=user_api_key, isNoneDoc=data.get("isNoneDoc"), + index=index, ), mimetype="text/event-stream", ) except ValueError: message = "Malformed request body" - print("\033[91merr", str(message), file=sys.stderr) + logger.error(f"/stream - error: {message}") return Response( error_stream_generate(message), status=400, mimetype="text/event-stream", ) except Exception as e: - current_app.logger.error( + logger.error( f"/stream - error: {str(e)} - traceback: {traceback.format_exc()}", extra={"error": str(e), "traceback": traceback.format_exc()}, ) - message = e.args[0] status_code = 400 - # Custom exceptions with two arguments, index 1 as status code - if len(e.args) >= 2: - status_code = e.args[1] return Response( - error_stream_generate(message), + error_stream_generate("Unknown error occurred"), status=status_code, mimetype="text/event-stream", ) @@ -421,14 +447,16 @@ class Answer(Resource): @api.doc(description="Provide an answer based on the question and retriever") def post(self): data = request.get_json() - required_fields = ["question"] + required_fields = ["question"] missing_fields = check_required_fields(data, required_fields) if missing_fields: return missing_fields try: question = data["question"] - history = data.get("history", []) + history = limit_chat_history( + json.loads(data.get("history", [])), gpt_model=gpt_model + ) conversation_id = data.get("conversation_id") prompt_id = data.get("prompt_id", "default") chunks = int(data.get("chunks", 2)) @@ -452,7 +480,7 @@ class Answer(Resource): prompt = get_prompt(prompt_id) - current_app.logger.info( + logger.info( f"/api/answer - request_data: {data}, source: {source}", extra={"data": json.dumps({"request_data": data, "source": source})}, ) @@ -469,13 +497,16 @@ class Answer(Resource): user_api_key=user_api_key, ) - source_log_docs = [] response_full = "" + source_log_docs = [] + tool_calls = [] for line in retriever.gen(): if "source" in line: source_log_docs.append(line["source"]) elif "answer" in line: response_full += line["answer"] + elif "tool_calls" in line: + tool_calls.append(line["tool_calls"]) if data.get("isNoneDoc"): for doc in source_log_docs: @@ -488,7 +519,12 @@ class Answer(Resource): result = {"answer": response_full, "sources": source_log_docs} result["conversation_id"] = str( save_conversation( - conversation_id, question, response_full, source_log_docs, llm + conversation_id, + question, + response_full, + source_log_docs, + tool_calls, + llm, ) ) retriever_params = retriever.get_params() @@ -507,7 +543,7 @@ class Answer(Resource): ) except Exception as e: - current_app.logger.error( + logger.error( f"/api/answer - error: {str(e)} - traceback: {traceback.format_exc()}", extra={"error": str(e), "traceback": traceback.format_exc()}, ) @@ -572,7 +608,7 @@ class Search(Resource): source = {} user_api_key = None - current_app.logger.info( + logger.info( f"/api/answer - request_data: {data}, source: {source}", extra={"data": json.dumps({"request_data": data, "source": source})}, ) @@ -610,7 +646,7 @@ class Search(Resource): doc["source"] = "None" except Exception as e: - current_app.logger.error( + logger.error( f"/api/search - error: {str(e)} - traceback: {traceback.format_exc()}", extra={"error": str(e), "traceback": traceback.format_exc()}, ) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 6a2f3bea..f71ab3dc 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -1,14 +1,15 @@ import datetime +import math import os import shutil import uuid -import math +import json from bson.binary import Binary, UuidRepresentation from bson.dbref import DBRef from bson.objectid import ObjectId -from flask import Blueprint, jsonify, make_response, request, redirect -from flask_restx import inputs, fields, Namespace, Resource +from flask import Blueprint, current_app, jsonify, make_response, redirect, request +from flask_restx import fields, inputs, Namespace, Resource from werkzeug.utils import secure_filename from application.api.user.tasks import ingest, ingest_remote @@ -16,9 +17,10 @@ from application.api.user.tasks import ingest, ingest_remote from application.core.mongo_db import MongoDB from application.core.settings import settings from application.extensions import api -from application.utils import check_required_fields -from application.vectorstore.vector_creator import VectorCreator +from application.tools.tool_manager import ToolManager from application.tts.google_tts import GoogleTTS +from application.utils import check_required_fields, validate_function_name +from application.vectorstore.vector_creator import VectorCreator mongo = MongoDB.get_client() db = mongo["docsgpt"] @@ -30,6 +32,7 @@ api_key_collection = db["api_keys"] token_usage_collection = db["token_usage"] shared_conversations_collections = db["shared_conversations"] user_logs_collection = db["user_logs"] +user_tools_collection = db["user_tools"] user = Blueprint("user", __name__) user_ns = Namespace("user", description="User related operations", path="/") @@ -39,6 +42,9 @@ current_dir = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) +tool_config = {} +tool_manager = ToolManager(config=tool_config) + def generate_minute_range(start_date, end_date): return { @@ -77,7 +83,8 @@ class DeleteConversation(Resource): try: conversations_collection.delete_one({"_id": ObjectId(conversation_id)}) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error deleting conversation: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -91,7 +98,8 @@ class DeleteAllConversations(Resource): try: conversations_collection.delete_many({"user": user_id}) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error deleting all conversations: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -108,7 +116,8 @@ class GetConversations(Resource): for conversation in conversations ] except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error retrieving conversations: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify(list_conversations), 200) @@ -132,7 +141,8 @@ class GetSingleConversation(Resource): if not conversation: return make_response(jsonify({"status": "not found"}), 404) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error retrieving conversation: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify(conversation["queries"]), 200) @@ -164,7 +174,8 @@ class UpdateConversationName(Resource): {"_id": ObjectId(data["id"])}, {"$set": {"name": data["name"]}} ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error updating conversation name: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -176,10 +187,17 @@ class SubmitFeedback(Resource): "FeedbackModel", { "question": fields.String( - required=True, description="The user question" + required=False, description="The user question" ), - "answer": fields.String(required=True, description="The AI answer"), + "answer": fields.String(required=False, description="The AI answer"), "feedback": fields.String(required=True, description="User feedback"), + "question_index": fields.Integer( + required=True, + description="The question number in that particular conversation", + ), + "conversation_id": fields.String( + required=True, description="id of the particular conversation" + ), "api_key": fields.String(description="Optional API key"), }, ) @@ -189,25 +207,27 @@ class SubmitFeedback(Resource): ) def post(self): data = request.get_json() - required_fields = ["question", "answer", "feedback"] + required_fields = ["feedback", "conversation_id", "question_index"] missing_fields = check_required_fields(data, required_fields) if missing_fields: return missing_fields - new_doc = { - "question": data["question"], - "answer": data["answer"], - "feedback": data["feedback"], - "timestamp": datetime.datetime.now(datetime.timezone.utc), - } - - if "api_key" in data: - new_doc["api_key"] = data["api_key"] - try: - feedback_collection.insert_one(new_doc) + conversations_collection.update_one( + { + "_id": ObjectId(data["conversation_id"]), + f"queries.{data['question_index']}": {"$exists": True}, + }, + { + "$set": { + f"queries.{data['question_index']}.feedback": data["feedback"] + } + }, + ) + except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error submitting feedback: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -230,7 +250,8 @@ class DeleteByIds(Resource): if result: return make_response(jsonify({"success": True}), 200) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error deleting indexes: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": False}), 400) @@ -248,13 +269,10 @@ class DeleteOldIndexes(Resource): jsonify({"success": False, "message": "Missing required fields"}), 400 ) + doc = sources_collection.find_one({"_id": ObjectId(source_id), "user": "local"}) + if not doc: + return make_response(jsonify({"status": "not found"}), 404) try: - doc = sources_collection.find_one( - {"_id": ObjectId(source_id), "user": "local"} - ) - if not doc: - return make_response(jsonify({"status": "not found"}), 404) - if settings.VECTOR_STORE == "faiss": shutil.rmtree(os.path.join(current_dir, "indexes", str(doc["_id"]))) else: @@ -263,12 +281,13 @@ class DeleteOldIndexes(Resource): ) vectorstore.delete_index() - sources_collection.delete_one({"_id": ObjectId(source_id)}) except FileNotFoundError: pass except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error deleting old indexes: {err}") + return make_response(jsonify({"success": False}), 400) + sources_collection.delete_one({"_id": ObjectId(source_id)}) return make_response(jsonify({"success": True}), 200) @@ -339,6 +358,9 @@ class UploadFile(Resource): ".json", ".xlsx", ".pptx", + ".png", + ".jpg", + ".jpeg", ], job_name, final_filename, @@ -365,6 +387,9 @@ class UploadFile(Resource): ".json", ".xlsx", ".pptx", + ".png", + ".jpg", + ".jpeg", ], job_name, final_filename, @@ -372,8 +397,8 @@ class UploadFile(Resource): ) except Exception as err: - print(f"Error: {err}") - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error uploading file: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True, "task_id": task.id}), 200) @@ -404,21 +429,26 @@ class UploadRemote(Resource): return missing_fields try: - if "repo_url" in data: - source_data = data["repo_url"] - loader = "github" - else: - source_data = data["data"] - loader = data["source"] + config = json.loads(data["data"]) + source_data = None - task = ingest_remote.delay( + if data["source"] == "github": + source_data = config.get("repo_url") + elif data["source"] in ["crawler", "url"]: + source_data = config.get("url") + elif data["source"] == "reddit": + source_data = config + + + task = ingest_remote.delay( source_data=source_data, job_name=data["name"], user=data["user"], - loader=loader, + loader=data["source"] ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error uploading remote source: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True, "task_id": task.id}), 200) @@ -450,7 +480,8 @@ class TaskStatus(Resource): ): task_meta = str(task_meta) # Convert to a string representation except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error getting task status: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"status": task.status, "result": task_meta}), 200) @@ -473,11 +504,24 @@ class PaginatedSources(Resource): sort_order = request.args.get("order", "desc") # Default to 'desc' page = int(request.args.get("page", 1)) # Default to 1 rows_per_page = int(request.args.get("rows", 10)) # Default to 10 + # add .strip() to remove leading and trailing whitespaces + search_term = request.args.get( + "search", "" + ).strip() # add search for filter documents - # Prepare + # Prepare query for filtering query = {"user": user} + if search_term: + query["name"] = { + "$regex": search_term, + "$options": "i", # using case-insensitive search + } + total_documents = sources_collection.count_documents(query) total_pages = max(1, math.ceil(total_documents / rows_per_page)) + page = min( + max(1, page), total_pages + ) # add this to make sure page inbound is within the range sort_order = 1 if sort_order == "asc" else -1 skip = (page - 1) * rows_per_page @@ -512,7 +556,8 @@ class PaginatedSources(Resource): return make_response(jsonify(response), 200) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error retrieving paginated sources: {err}") + return make_response(jsonify({"success": False}), 400) @user_ns.route("/api/sources") @@ -522,7 +567,7 @@ class CombinedJson(Resource): user = "local" data = [ { - "name": "default", + "name": "Default", "date": "default", "model": settings.EMBEDDINGS_NAME, "location": "remote", @@ -572,7 +617,8 @@ class CombinedJson(Resource): ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error retrieving sources: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify(data), 200) @@ -598,7 +644,8 @@ class CheckDocs(Resource): if os.path.exists(vectorstore) or data["docs"] == "default": return {"status": "exists"}, 200 except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error checking document: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"status": "not found"}), 404) @@ -636,7 +683,8 @@ class CreatePrompt(Resource): ) new_id = str(resp.inserted_id) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error creating prompt: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"id": new_id}), 200) @@ -663,7 +711,8 @@ class GetPrompts(Resource): } ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error retrieving prompts: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify(list_prompts), 200) @@ -704,7 +753,8 @@ class GetSinglePrompt(Resource): prompt = prompts_collection.find_one({"_id": ObjectId(prompt_id)}) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error retrieving prompt: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"content": prompt["content"]}), 200) @@ -728,7 +778,8 @@ class DeletePrompt(Resource): try: prompts_collection.delete_one({"_id": ObjectId(data["id"])}) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error deleting prompt: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -761,7 +812,8 @@ class UpdatePrompt(Resource): {"$set": {"name": data["name"], "content": data["content"]}}, ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error updating prompt: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -796,7 +848,8 @@ class GetApiKeys(Resource): } ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error retrieving API keys: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify(list_keys), 200) @@ -840,7 +893,8 @@ class CreateApiKey(Resource): resp = api_key_collection.insert_one(new_api_key) new_id = str(resp.inserted_id) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error creating API key: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"id": new_id, "key": key}), 201) @@ -866,7 +920,8 @@ class DeleteApiKey(Resource): if result.deleted_count == 0: return {"success": False, "message": "API Key not found"}, 404 except Exception as err: - return {"success": False, "error": str(err)}, 400 + current_app.logger.error(f"Error deleting API key: {err}") + return {"success": False}, 400 return {"success": True}, 200 @@ -1066,7 +1121,8 @@ class ShareConversation(Resource): 201, ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error sharing conversation: {err}") + return make_response(jsonify({"success": False}), 400) @user_ns.route("/api/shared_conversation/") @@ -1121,7 +1177,8 @@ class GetPubliclySharedConversations(Resource): res["api_key"] = shared["api_key"] return make_response(jsonify(res), 200) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error getting shared conversation: {err}") + return make_response(jsonify({"success": False}), 400) @user_ns.route("/api/get_message_analytics") @@ -1162,7 +1219,8 @@ class GetMessageAnalytics(Resource): else None ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error getting API key: {err}") + return make_response(jsonify({"success": False}), 400) end_date = datetime.datetime.now(datetime.timezone.utc) if filter_option == "last_hour": @@ -1255,7 +1313,8 @@ class GetMessageAnalytics(Resource): daily_messages[entry["_id"]["day"]] = entry["total_messages"] except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error getting message analytics: {err}") + return make_response(jsonify({"success": False}), 400) return make_response( jsonify({"success": True, "messages": daily_messages}), 200 @@ -1297,7 +1356,8 @@ class GetTokenAnalytics(Resource): else None ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error getting API key: {err}") + return make_response(jsonify({"success": False}), 400) end_date = datetime.datetime.now(datetime.timezone.utc) if filter_option == "last_hour": @@ -1406,7 +1466,8 @@ class GetTokenAnalytics(Resource): daily_token_usage[entry["_id"]["day"]] = entry["total_tokens"] except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error getting token analytics: {err}") + return make_response(jsonify({"success": False}), 400) return make_response( jsonify({"success": True, "token_usage": daily_token_usage}), 200 @@ -1448,91 +1509,19 @@ class GetFeedbackAnalytics(Resource): else None ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error getting API key: {err}") + return make_response(jsonify({"success": False}), 400) + end_date = datetime.datetime.now(datetime.timezone.utc) if filter_option == "last_hour": start_date = end_date - datetime.timedelta(hours=1) group_format = "%Y-%m-%d %H:%M:00" - group_stage_1 = { - "$group": { - "_id": { - "minute": { - "$dateToString": { - "format": group_format, - "date": "$timestamp", - } - }, - "feedback": "$feedback", - }, - "count": {"$sum": 1}, - } - } - group_stage_2 = { - "$group": { - "_id": "$_id.minute", - "likes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "LIKE"]}, - "$count", - 0, - ] - } - }, - "dislikes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "DISLIKE"]}, - "$count", - 0, - ] - } - }, - } - } - + date_field = {"$dateToString": {"format": group_format, "date": "$date"}} elif filter_option == "last_24_hour": start_date = end_date - datetime.timedelta(hours=24) group_format = "%Y-%m-%d %H:00" - group_stage_1 = { - "$group": { - "_id": { - "hour": { - "$dateToString": { - "format": group_format, - "date": "$timestamp", - } - }, - "feedback": "$feedback", - }, - "count": {"$sum": 1}, - } - } - group_stage_2 = { - "$group": { - "_id": "$_id.hour", - "likes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "LIKE"]}, - "$count", - 0, - ] - } - }, - "dislikes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "DISLIKE"]}, - "$count", - 0, - ] - } - }, - } - } - + date_field = {"$dateToString": {"format": group_format, "date": "$date"}} else: if filter_option in ["last_7_days", "last_15_days", "last_30_days"]: filter_days = ( @@ -1550,61 +1539,56 @@ class GetFeedbackAnalytics(Resource): hour=23, minute=59, second=59, microsecond=999999 ) group_format = "%Y-%m-%d" - group_stage_1 = { - "$group": { - "_id": { - "day": { - "$dateToString": { - "format": group_format, - "date": "$timestamp", - } - }, - "feedback": "$feedback", - }, - "count": {"$sum": 1}, - } - } - group_stage_2 = { - "$group": { - "_id": "$_id.day", - "likes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "LIKE"]}, - "$count", - 0, - ] - } - }, - "dislikes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "DISLIKE"]}, - "$count", - 0, - ] - } - }, - } - } + date_field = {"$dateToString": {"format": group_format, "date": "$date"}} try: match_stage = { "$match": { - "timestamp": {"$gte": start_date, "$lte": end_date}, + "date": {"$gte": start_date, "$lte": end_date}, + "queries": {"$exists": True, "$ne": []}, } } if api_key: match_stage["$match"]["api_key"] = api_key - feedback_data = feedback_collection.aggregate( - [ - match_stage, - group_stage_1, - group_stage_2, - {"$sort": {"_id": 1}}, - ] - ) + # Unwind the queries array to process each query separately + pipeline = [ + match_stage, + {"$unwind": "$queries"}, + {"$match": {"queries.feedback": {"$exists": True}}}, + { + "$group": { + "_id": {"time": date_field, "feedback": "$queries.feedback"}, + "count": {"$sum": 1}, + } + }, + { + "$group": { + "_id": "$_id.time", + "positive": { + "$sum": { + "$cond": [ + {"$eq": ["$_id.feedback", "LIKE"]}, + "$count", + 0, + ] + } + }, + "negative": { + "$sum": { + "$cond": [ + {"$eq": ["$_id.feedback", "DISLIKE"]}, + "$count", + 0, + ] + } + }, + } + }, + {"$sort": {"_id": 1}}, + ] + + feedback_data = conversations_collection.aggregate(pipeline) if filter_option == "last_hour": intervals = generate_minute_range(start_date, end_date) @@ -1619,12 +1603,13 @@ class GetFeedbackAnalytics(Resource): for entry in feedback_data: daily_feedback[entry["_id"]] = { - "positive": entry["likes"], - "negative": entry["dislikes"], + "positive": entry["positive"], + "negative": entry["negative"], } except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error getting feedback analytics: {err}") + return make_response(jsonify({"success": False}), 400) return make_response( jsonify({"success": True, "feedback": daily_feedback}), 200 @@ -1666,7 +1651,8 @@ class GetUserLogs(Resource): else None ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error getting API key: {err}") + return make_response(jsonify({"success": False}), 400) query = {} if api_key: @@ -1750,7 +1736,8 @@ class ManageSync(Resource): update_data, ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error updating sync frequency: {err}") + return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -1785,4 +1772,514 @@ class TextToSpeech(Resource): 200, ) except Exception as err: - return make_response(jsonify({"success": False, "error": str(err)}), 400) + current_app.logger.error(f"Error synthesizing audio: {err}") + return make_response(jsonify({"success": False}), 400) + + +@user_ns.route("/api/available_tools") +class AvailableTools(Resource): + @api.doc(description="Get available tools for a user") + def get(self): + try: + tools_metadata = [] + for tool_name, tool_instance in tool_manager.tools.items(): + doc = tool_instance.__doc__.strip() + lines = doc.split("\n", 1) + name = lines[0].strip() + description = lines[1].strip() if len(lines) > 1 else "" + tools_metadata.append( + { + "name": tool_name, + "displayName": name, + "description": description, + "configRequirements": tool_instance.get_config_requirements(), + "actions": tool_instance.get_actions_metadata(), + } + ) + except Exception as err: + current_app.logger.error(f"Error getting available tools: {err}") + return make_response(jsonify({"success": False}), 400) + + return make_response(jsonify({"success": True, "data": tools_metadata}), 200) + + +@user_ns.route("/api/get_tools") +class GetTools(Resource): + @api.doc(description="Get tools created by a user") + def get(self): + try: + user = "local" + tools = user_tools_collection.find({"user": user}) + user_tools = [] + for tool in tools: + tool["id"] = str(tool["_id"]) + tool.pop("_id") + user_tools.append(tool) + except Exception as err: + current_app.logger.error(f"Error getting user tools: {err}") + return make_response(jsonify({"success": False}), 400) + + return make_response(jsonify({"success": True, "tools": user_tools}), 200) + + +@user_ns.route("/api/create_tool") +class CreateTool(Resource): + @api.expect( + api.model( + "CreateToolModel", + { + "name": fields.String(required=True, description="Name of the tool"), + "displayName": fields.String( + required=True, description="Display name for the tool" + ), + "description": fields.String( + required=True, description="Tool description" + ), + "config": fields.Raw( + required=True, description="Configuration of the tool" + ), + "actions": fields.List( + fields.Raw, + required=True, + description="Actions the tool can perform", + ), + "status": fields.Boolean( + required=True, description="Status of the tool" + ), + }, + ) + ) + @api.doc(description="Create a new tool") + def post(self): + data = request.get_json() + required_fields = [ + "name", + "displayName", + "description", + "actions", + "config", + "status", + ] + missing_fields = check_required_fields(data, required_fields) + if missing_fields: + return missing_fields + + user = "local" + transformed_actions = [] + for action in data["actions"]: + action["active"] = True + if "parameters" in action: + if "properties" in action["parameters"]: + for param_name, param_details in action["parameters"][ + "properties" + ].items(): + param_details["filled_by_llm"] = True + param_details["value"] = "" + transformed_actions.append(action) + try: + new_tool = { + "user": user, + "name": data["name"], + "displayName": data["displayName"], + "description": data["description"], + "actions": transformed_actions, + "config": data["config"], + "status": data["status"], + } + resp = user_tools_collection.insert_one(new_tool) + new_id = str(resp.inserted_id) + except Exception as err: + current_app.logger.error(f"Error creating tool: {err}") + return make_response(jsonify({"success": False}), 400) + + return make_response(jsonify({"id": new_id}), 200) + + +@user_ns.route("/api/update_tool") +class UpdateTool(Resource): + @api.expect( + api.model( + "UpdateToolModel", + { + "id": fields.String(required=True, description="Tool ID"), + "name": fields.String(description="Name of the tool"), + "displayName": fields.String(description="Display name for the tool"), + "description": fields.String(description="Tool description"), + "config": fields.Raw(description="Configuration of the tool"), + "actions": fields.List( + fields.Raw, description="Actions the tool can perform" + ), + "status": fields.Boolean(description="Status of the tool"), + }, + ) + ) + @api.doc(description="Update a tool by ID") + def post(self): + data = request.get_json() + required_fields = ["id"] + missing_fields = check_required_fields(data, required_fields) + if missing_fields: + return missing_fields + + try: + update_data = {} + if "name" in data: + update_data["name"] = data["name"] + if "displayName" in data: + update_data["displayName"] = data["displayName"] + if "description" in data: + update_data["description"] = data["description"] + if "actions" in data: + update_data["actions"] = data["actions"] + if "config" in data: + if "actions" in data["config"]: + for action_name in list(data["config"]["actions"].keys()): + if not validate_function_name(action_name): + return make_response( + jsonify({ + "success": False, + "message": f"Invalid function name '{action_name}'. Function names must match pattern '^[a-zA-Z0-9_-]+$'.", + "param": "tools[].function.name" + }), 400 + ) + update_data["config"] = data["config"] + if "status" in data: + update_data["status"] = data["status"] + + user_tools_collection.update_one( + {"_id": ObjectId(data["id"]), "user": "local"}, + {"$set": update_data}, + ) + except Exception as err: + current_app.logger.error(f"Error updating tool: {err}") + return make_response(jsonify({"success": False}), 400) + + return make_response(jsonify({"success": True}), 200) + + +@user_ns.route("/api/update_tool_config") +class UpdateToolConfig(Resource): + @api.expect( + api.model( + "UpdateToolConfigModel", + { + "id": fields.String(required=True, description="Tool ID"), + "config": fields.Raw( + required=True, description="Configuration of the tool" + ), + }, + ) + ) + @api.doc(description="Update the configuration of a tool") + def post(self): + data = request.get_json() + required_fields = ["id", "config"] + missing_fields = check_required_fields(data, required_fields) + if missing_fields: + return missing_fields + + try: + user_tools_collection.update_one( + {"_id": ObjectId(data["id"])}, + {"$set": {"config": data["config"]}}, + ) + except Exception as err: + current_app.logger.error(f"Error updating tool config: {err}") + return make_response(jsonify({"success": False}), 400) + + return make_response(jsonify({"success": True}), 200) + + +@user_ns.route("/api/update_tool_actions") +class UpdateToolActions(Resource): + @api.expect( + api.model( + "UpdateToolActionsModel", + { + "id": fields.String(required=True, description="Tool ID"), + "actions": fields.List( + fields.Raw, + required=True, + description="Actions the tool can perform", + ), + }, + ) + ) + @api.doc(description="Update the actions of a tool") + def post(self): + data = request.get_json() + required_fields = ["id", "actions"] + missing_fields = check_required_fields(data, required_fields) + if missing_fields: + return missing_fields + + try: + user_tools_collection.update_one( + {"_id": ObjectId(data["id"])}, + {"$set": {"actions": data["actions"]}}, + ) + except Exception as err: + current_app.logger.error(f"Error updating tool actions: {err}") + return make_response(jsonify({"success": False}), 400) + + return make_response(jsonify({"success": True}), 200) + + +@user_ns.route("/api/update_tool_status") +class UpdateToolStatus(Resource): + @api.expect( + api.model( + "UpdateToolStatusModel", + { + "id": fields.String(required=True, description="Tool ID"), + "status": fields.Boolean( + required=True, description="Status of the tool" + ), + }, + ) + ) + @api.doc(description="Update the status of a tool") + def post(self): + data = request.get_json() + required_fields = ["id", "status"] + missing_fields = check_required_fields(data, required_fields) + if missing_fields: + return missing_fields + + try: + user_tools_collection.update_one( + {"_id": ObjectId(data["id"])}, + {"$set": {"status": data["status"]}}, + ) + except Exception as err: + current_app.logger.error(f"Error updating tool status: {err}") + return make_response(jsonify({"success": False}), 400) + + return make_response(jsonify({"success": True}), 200) + + +@user_ns.route("/api/delete_tool") +class DeleteTool(Resource): + @api.expect( + api.model( + "DeleteToolModel", + {"id": fields.String(required=True, description="Tool ID")}, + ) + ) + @api.doc(description="Delete a tool by ID") + def post(self): + data = request.get_json() + required_fields = ["id"] + missing_fields = check_required_fields(data, required_fields) + if missing_fields: + return missing_fields + + try: + result = user_tools_collection.delete_one({"_id": ObjectId(data["id"])}) + if result.deleted_count == 0: + return {"success": False, "message": "Tool not found"}, 404 + except Exception as err: + current_app.logger.error(f"Error deleting tool: {err}") + return {"success": False}, 400 + + return {"success": True}, 200 + + +def get_vector_store(source_id): + """ + Get the Vector Store + Args: + source_id (str): source id of the document + """ + + store = VectorCreator.create_vectorstore( + settings.VECTOR_STORE, + source_id=source_id, + embeddings_key=os.getenv("EMBEDDINGS_KEY"), + ) + return store + + +@user_ns.route("/api/get_chunks") +class GetChunks(Resource): + @api.doc( + description="Retrieves all chunks associated with a document", + params={"id": "The document ID"}, + ) + def get(self): + doc_id = request.args.get("id") + page = int(request.args.get("page", 1)) + per_page = int(request.args.get("per_page", 10)) + + if not ObjectId.is_valid(doc_id): + return make_response(jsonify({"error": "Invalid doc_id"}), 400) + + try: + store = get_vector_store(doc_id) + chunks = store.get_chunks() + total_chunks = len(chunks) + start = (page - 1) * per_page + end = start + per_page + paginated_chunks = chunks[start:end] + + return make_response( + jsonify( + { + "page": page, + "per_page": per_page, + "total": total_chunks, + "chunks": paginated_chunks, + } + ), + 200, + ) + + except Exception as e: + current_app.logger.error(f"Error getting chunks: {e}") + return make_response(jsonify({"success": False}), 500) + + +@user_ns.route("/api/add_chunk") +class AddChunk(Resource): + @api.expect( + api.model( + "AddChunkModel", + { + "id": fields.String(required=True, description="Document ID"), + "text": fields.String(required=True, description="Text of the chunk"), + "metadata": fields.Raw( + required=False, + description="Metadata associated with the chunk", + ), + }, + ) + ) + @api.doc( + description="Adds a new chunk to the document", + ) + def post(self): + data = request.get_json() + required_fields = ["id", "text"] + missing_fields = check_required_fields(data, required_fields) + if missing_fields: + return missing_fields + + doc_id = data.get("id") + text = data.get("text") + metadata = data.get("metadata", {}) + + if not ObjectId.is_valid(doc_id): + return make_response(jsonify({"error": "Invalid doc_id"}), 400) + + try: + store = get_vector_store(doc_id) + chunk_id = store.add_chunk(text, metadata) + return make_response( + jsonify({"message": "Chunk added successfully", "chunk_id": chunk_id}), + 201, + ) + except Exception as e: + current_app.logger.error(f"Error adding chunk: {e}") + return make_response(jsonify({"success": False}), 500) + + +@user_ns.route("/api/delete_chunk") +class DeleteChunk(Resource): + @api.doc( + description="Deletes a specific chunk from the document.", + params={"id": "The document ID", "chunk_id": "The ID of the chunk to delete"}, + ) + def delete(self): + doc_id = request.args.get("id") + chunk_id = request.args.get("chunk_id") + + if not ObjectId.is_valid(doc_id): + return make_response(jsonify({"error": "Invalid doc_id"}), 400) + + try: + store = get_vector_store(doc_id) + deleted = store.delete_chunk(chunk_id) + if deleted: + return make_response( + jsonify({"message": "Chunk deleted successfully"}), 200 + ) + else: + return make_response( + jsonify({"message": "Chunk not found or could not be deleted"}), + 404, + ) + except Exception as e: + current_app.logger.error(f"Error deleting chunk: {e}") + return make_response(jsonify({"success": False}), 500) + + +@user_ns.route("/api/update_chunk") +class UpdateChunk(Resource): + @api.expect( + api.model( + "UpdateChunkModel", + { + "id": fields.String(required=True, description="Document ID"), + "chunk_id": fields.String( + required=True, description="Chunk ID to update" + ), + "text": fields.String( + required=False, description="New text of the chunk" + ), + "metadata": fields.Raw( + required=False, + description="Updated metadata associated with the chunk", + ), + }, + ) + ) + @api.doc( + description="Updates an existing chunk in the document.", + ) + def put(self): + data = request.get_json() + required_fields = ["id", "chunk_id"] + missing_fields = check_required_fields(data, required_fields) + if missing_fields: + return missing_fields + + doc_id = data.get("id") + chunk_id = data.get("chunk_id") + text = data.get("text") + metadata = data.get("metadata") + + if not ObjectId.is_valid(doc_id): + return make_response(jsonify({"error": "Invalid doc_id"}), 400) + + try: + store = get_vector_store(doc_id) + chunks = store.get_chunks() + existing_chunk = next((c for c in chunks if c["doc_id"] == chunk_id), None) + if not existing_chunk: + return make_response(jsonify({"error": "Chunk not found"}), 404) + + deleted = store.delete_chunk(chunk_id) + if not deleted: + return make_response( + jsonify({"error": "Failed to delete existing chunk"}), 500 + ) + + new_text = text if text is not None else existing_chunk["text"] + new_metadata = ( + metadata if metadata is not None else existing_chunk["metadata"] + ) + + new_chunk_id = store.add_chunk(new_text, new_metadata) + + return make_response( + jsonify( + { + "message": "Chunk updated successfully", + "new_chunk_id": new_chunk_id, + } + ), + 200, + ) + except Exception as e: + current_app.logger.error(f"Error updating chunk: {e}") + return make_response(jsonify({"success": False}), 500) diff --git a/application/app.py b/application/app.py index d7727001..4eb40331 100644 --- a/application/app.py +++ b/application/app.py @@ -2,22 +2,22 @@ import platform import dotenv from flask import Flask, redirect, request - -from application.api.answer.routes import answer -from application.api.internal.routes import internal -from application.api.user.routes import user -from application.celery_init import celery from application.core.logging_config import setup_logging -from application.core.settings import settings -from application.extensions import api +setup_logging() + +from application.api.answer.routes import answer # noqa: E402 +from application.api.internal.routes import internal # noqa: E402 +from application.api.user.routes import user # noqa: E402 +from application.celery_init import celery # noqa: E402 +from application.core.settings import settings # noqa: E402 +from application.extensions import api # noqa: E402 + if platform.system() == "Windows": import pathlib - pathlib.PosixPath = pathlib.WindowsPath dotenv.load_dotenv() -setup_logging() app = Flask(__name__) app.register_blueprint(user) diff --git a/application/cache.py b/application/cache.py index 33022e45..80dee4f4 100644 --- a/application/cache.py +++ b/application/cache.py @@ -1,8 +1,10 @@ -import redis -import time import json import logging +import time from threading import Lock + +import redis + from application.core.settings import settings from application.utils import get_hash @@ -11,41 +13,47 @@ logger = logging.getLogger(__name__) _redis_instance = None _instance_lock = Lock() + def get_redis_instance(): global _redis_instance if _redis_instance is None: with _instance_lock: if _redis_instance is None: try: - _redis_instance = redis.Redis.from_url(settings.CACHE_REDIS_URL, socket_connect_timeout=2) + _redis_instance = redis.Redis.from_url( + settings.CACHE_REDIS_URL, socket_connect_timeout=2 + ) except redis.ConnectionError as e: logger.error(f"Redis connection error: {e}") _redis_instance = None return _redis_instance -def gen_cache_key(*messages, model="docgpt"): + +def gen_cache_key(messages, model="docgpt", tools=None): if not all(isinstance(msg, dict) for msg in messages): raise ValueError("All messages must be dictionaries.") - messages_str = json.dumps(list(messages), sort_keys=True) - combined = f"{model}_{messages_str}" + messages_str = json.dumps(messages) + tools_str = json.dumps(str(tools)) if tools else "" + combined = f"{model}_{messages_str}_{tools_str}" cache_key = get_hash(combined) return cache_key + def gen_cache(func): - def wrapper(self, model, messages, *args, **kwargs): + def wrapper(self, model, messages, stream, tools=None, *args, **kwargs): try: - cache_key = gen_cache_key(*messages) + cache_key = gen_cache_key(messages, model, tools) redis_client = get_redis_instance() if redis_client: try: cached_response = redis_client.get(cache_key) if cached_response: - return cached_response.decode('utf-8') + return cached_response.decode("utf-8") except redis.ConnectionError as e: logger.error(f"Redis connection error: {e}") - result = func(self, model, messages, *args, **kwargs) - if redis_client: + result = func(self, model, messages, stream, tools, *args, **kwargs) + if redis_client and isinstance(result, str): try: redis_client.set(cache_key, result, ex=1800) except redis.ConnectionError as e: @@ -55,20 +63,22 @@ def gen_cache(func): except ValueError as e: logger.error(e) return "Error: No user message found in the conversation to generate a cache key." + return wrapper + def stream_cache(func): - def wrapper(self, model, messages, stream, *args, **kwargs): - cache_key = gen_cache_key(*messages) + def wrapper(self, model, messages, stream, tools=None, *args, **kwargs): + cache_key = gen_cache_key(messages, model, tools) logger.info(f"Stream cache key: {cache_key}") - + redis_client = get_redis_instance() if redis_client: try: cached_response = redis_client.get(cache_key) if cached_response: logger.info(f"Cache hit for stream key: {cache_key}") - cached_response = json.loads(cached_response.decode('utf-8')) + cached_response = json.loads(cached_response.decode("utf-8")) for chunk in cached_response: yield chunk time.sleep(0.03) @@ -76,18 +86,18 @@ def stream_cache(func): except redis.ConnectionError as e: logger.error(f"Redis connection error: {e}") - result = func(self, model, messages, stream, *args, **kwargs) + result = func(self, model, messages, stream, tools=tools, *args, **kwargs) stream_cache_data = [] - + for chunk in result: stream_cache_data.append(chunk) yield chunk - + if redis_client: try: redis_client.set(cache_key, json.dumps(stream_cache_data), ex=1800) logger.info(f"Stream cache saved for key: {cache_key}") except redis.ConnectionError as e: logger.error(f"Redis connection error: {e}") - - return wrapper \ No newline at end of file + + return wrapper diff --git a/application/celery_init.py b/application/celery_init.py index c5838083..185cc87f 100644 --- a/application/celery_init.py +++ b/application/celery_init.py @@ -2,14 +2,22 @@ from celery import Celery from application.core.settings import settings from celery.signals import setup_logging + def make_celery(app_name=__name__): - celery = Celery(app_name, broker=settings.CELERY_BROKER_URL, backend=settings.CELERY_RESULT_BACKEND) + celery = Celery( + app_name, + broker=settings.CELERY_BROKER_URL, + backend=settings.CELERY_RESULT_BACKEND, + ) celery.conf.update(settings) return celery + @setup_logging.connect def config_loggers(*args, **kwargs): from application.core.logging_config import setup_logging + setup_logging() + celery = make_celery() diff --git a/application/core/settings.py b/application/core/settings.py index d4b02481..5842da33 100644 --- a/application/core/settings.py +++ b/application/core/settings.py @@ -1,25 +1,37 @@ +import os from pathlib import Path from typing import Optional -import os from pydantic_settings import BaseSettings -current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +current_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +) class Settings(BaseSettings): LLM_NAME: str = "docsgpt" - MODEL_NAME: Optional[str] = None # if LLM_NAME is openai, MODEL_NAME can be gpt-4 or gpt-3.5-turbo + MODEL_NAME: Optional[str] = ( + None # if LLM_NAME is openai, MODEL_NAME can be gpt-4 or gpt-3.5-turbo + ) EMBEDDINGS_NAME: str = "huggingface_sentence-transformers/all-mpnet-base-v2" CELERY_BROKER_URL: str = "redis://localhost:6379/0" CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1" MONGO_URI: str = "mongodb://localhost:27017/docsgpt" MODEL_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf") DEFAULT_MAX_HISTORY: int = 150 - MODEL_TOKEN_LIMITS: dict = {"gpt-3.5-turbo": 4096, "claude-2": 1e5} + MODEL_TOKEN_LIMITS: dict = { + "gpt-4o-mini": 128000, + "gpt-3.5-turbo": 4096, + "claude-2": 1e5, + "gemini-2.0-flash-exp": 1e6, + } UPLOAD_FOLDER: str = "inputs" - VECTOR_STORE: str = "faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" or "lancedb" - RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search + PARSE_PDF_AS_IMAGE: bool = False + VECTOR_STORE: str = ( + "faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" or "lancedb" + ) + RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search # LLM Cache CACHE_REDIS_URL: str = "redis://localhost:6379/2" @@ -27,12 +39,18 @@ class Settings(BaseSettings): API_URL: str = "http://localhost:7091" # backend url for celery worker API_KEY: Optional[str] = None # LLM api key - EMBEDDINGS_KEY: Optional[str] = None # api key for embeddings (if using openai, just copy API_KEY) + EMBEDDINGS_KEY: Optional[str] = ( + None # api key for embeddings (if using openai, just copy API_KEY) + ) OPENAI_API_BASE: Optional[str] = None # azure openai api base url OPENAI_API_VERSION: Optional[str] = None # azure openai api version AZURE_DEPLOYMENT_NAME: Optional[str] = None # azure deployment name for answering - AZURE_EMBEDDINGS_DEPLOYMENT_NAME: Optional[str] = None # azure deployment name for embeddings - OPENAI_BASE_URL: Optional[str] = None # openai base url for open ai compatable models + AZURE_EMBEDDINGS_DEPLOYMENT_NAME: Optional[str] = ( + None # azure deployment name for embeddings + ) + OPENAI_BASE_URL: Optional[str] = ( + None # openai base url for open ai compatable models + ) # elasticsearch ELASTIC_CLOUD_ID: Optional[str] = None # cloud id for elasticsearch @@ -67,12 +85,14 @@ class Settings(BaseSettings): # Milvus vectorstore config MILVUS_COLLECTION_NAME: Optional[str] = "docsgpt" - MILVUS_URI: Optional[str] = "./milvus_local.db" # milvus lite version as default + MILVUS_URI: Optional[str] = "./milvus_local.db" # milvus lite version as default MILVUS_TOKEN: Optional[str] = "" # LanceDB vectorstore config LANCEDB_PATH: str = "/tmp/lancedb" # Path where LanceDB stores its local data - LANCEDB_TABLE_NAME: Optional[str] = "docsgpts" # Name of the table to use for storing vectors + LANCEDB_TABLE_NAME: Optional[str] = ( + "docsgpts" # Name of the table to use for storing vectors + ) BRAVE_SEARCH_API_KEY: Optional[str] = None FLASK_DEBUG_MODE: bool = False diff --git a/application/llm/anthropic.py b/application/llm/anthropic.py index 4081bcd0..1fa3b5b2 100644 --- a/application/llm/anthropic.py +++ b/application/llm/anthropic.py @@ -17,7 +17,7 @@ class AnthropicLLM(BaseLLM): self.AI_PROMPT = AI_PROMPT def _raw_gen( - self, baseself, model, messages, stream=False, max_tokens=300, **kwargs + self, baseself, model, messages, stream=False, tools=None, max_tokens=300, **kwargs ): context = messages[0]["content"] user_question = messages[-1]["content"] @@ -34,7 +34,7 @@ class AnthropicLLM(BaseLLM): return completion.completion def _raw_gen_stream( - self, baseself, model, messages, stream=True, max_tokens=300, **kwargs + self, baseself, model, messages, stream=True, tools=None, max_tokens=300, **kwargs ): context = messages[0]["content"] user_question = messages[-1]["content"] diff --git a/application/llm/base.py b/application/llm/base.py index 1caab5d3..e687e567 100644 --- a/application/llm/base.py +++ b/application/llm/base.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod + +from application.cache import gen_cache, stream_cache from application.usage import gen_token_usage, stream_token_usage -from application.cache import stream_cache, gen_cache class BaseLLM(ABC): @@ -13,17 +14,43 @@ class BaseLLM(ABC): return method(self, *args, **kwargs) @abstractmethod - def _raw_gen(self, model, messages, stream, *args, **kwargs): + def _raw_gen(self, model, messages, stream, tools, *args, **kwargs): pass - def gen(self, model, messages, stream=False, *args, **kwargs): + def gen(self, model, messages, stream=False, tools=None, *args, **kwargs): decorators = [gen_token_usage, gen_cache] - return self._apply_decorator(self._raw_gen, decorators=decorators, model=model, messages=messages, stream=stream, *args, **kwargs) + return self._apply_decorator( + self._raw_gen, + decorators=decorators, + model=model, + messages=messages, + stream=stream, + tools=tools, + *args, + **kwargs + ) @abstractmethod def _raw_gen_stream(self, model, messages, stream, *args, **kwargs): pass - def gen_stream(self, model, messages, stream=True, *args, **kwargs): + def gen_stream(self, model, messages, stream=True, tools=None, *args, **kwargs): decorators = [stream_cache, stream_token_usage] - return self._apply_decorator(self._raw_gen_stream, decorators=decorators, model=model, messages=messages, stream=stream, *args, **kwargs) \ No newline at end of file + return self._apply_decorator( + self._raw_gen_stream, + decorators=decorators, + model=model, + messages=messages, + stream=stream, + tools=tools, + *args, + **kwargs + ) + + def supports_tools(self): + return hasattr(self, "_supports_tools") and callable( + getattr(self, "_supports_tools") + ) + + def _supports_tools(self): + raise NotImplementedError("Subclass must implement _supports_tools method") diff --git a/application/llm/docsgpt_provider.py b/application/llm/docsgpt_provider.py index bca39729..bb23d824 100644 --- a/application/llm/docsgpt_provider.py +++ b/application/llm/docsgpt_provider.py @@ -9,35 +9,25 @@ class DocsGPTAPILLM(BaseLLM): super().__init__(*args, **kwargs) self.api_key = api_key self.user_api_key = user_api_key - self.endpoint = "https://llm.docsgpt.co.uk" + self.endpoint = "https://llm.arc53.com" def _raw_gen(self, baseself, model, messages, stream=False, *args, **kwargs): - context = messages[0]["content"] - user_question = messages[-1]["content"] - prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n" - response = requests.post( - f"{self.endpoint}/answer", json={"prompt": prompt, "max_new_tokens": 30} + f"{self.endpoint}/answer", json={"messages": messages, "max_new_tokens": 30} ) response_clean = response.json()["a"].replace("###", "") return response_clean def _raw_gen_stream(self, baseself, model, messages, stream=True, *args, **kwargs): - context = messages[0]["content"] - user_question = messages[-1]["content"] - prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n" - - # send prompt to endpoint /stream response = requests.post( f"{self.endpoint}/stream", - json={"prompt": prompt, "max_new_tokens": 256}, + json={"messages": messages, "max_new_tokens": 256}, stream=True, ) for line in response.iter_lines(): if line: - # data = json.loads(line) data_str = line.decode("utf-8") if data_str.startswith("data: "): data = json.loads(data_str[6:]) diff --git a/application/llm/google_ai.py b/application/llm/google_ai.py index df252abf..31943601 100644 --- a/application/llm/google_ai.py +++ b/application/llm/google_ai.py @@ -1,21 +1,95 @@ +from google import genai +from google.genai import types + from application.llm.base import BaseLLM + class GoogleLLM(BaseLLM): - def __init__(self, api_key=None, user_api_key=None, *args, **kwargs): - super().__init__(*args, **kwargs) self.api_key = api_key self.user_api_key = user_api_key def _clean_messages_google(self, messages): - return [ - { - "role": "model" if message["role"] == "system" else message["role"], - "parts": [message["content"]], - } - for message in messages[1:] - ] + cleaned_messages = [] + for message in messages: + role = message.get("role") + content = message.get("content") + + if role == "assistant": + role = "model" + + parts = [] + if role and content is not None: + if isinstance(content, str): + parts = [types.Part.from_text(content)] + elif isinstance(content, list): + for item in content: + if "text" in item: + parts.append(types.Part.from_text(item["text"])) + elif "function_call" in item: + parts.append( + types.Part.from_function_call( + name=item["function_call"]["name"], + args=item["function_call"]["args"], + ) + ) + elif "function_response" in item: + parts.append( + types.Part.from_function_response( + name=item["function_response"]["name"], + response=item["function_response"]["response"], + ) + ) + else: + raise ValueError( + f"Unexpected content dictionary format:{item}" + ) + else: + raise ValueError(f"Unexpected content type: {type(content)}") + + cleaned_messages.append(types.Content(role=role, parts=parts)) + + return cleaned_messages + + def _clean_tools_format(self, tools_list): + genai_tools = [] + for tool_data in tools_list: + if tool_data["type"] == "function": + function = tool_data["function"] + parameters = function["parameters"] + properties = parameters.get("properties", {}) + + if properties: + genai_function = dict( + name=function["name"], + description=function["description"], + parameters={ + "type": "OBJECT", + "properties": { + k: { + **v, + "type": v["type"].upper() if v["type"] else None, + } + for k, v in properties.items() + }, + "required": ( + parameters["required"] + if "required" in parameters + else [] + ), + }, + ) + else: + genai_function = dict( + name=function["name"], + description=function["description"], + ) + + genai_tool = types.Tool(function_declarations=[genai_function]) + genai_tools.append(genai_tool) + + return genai_tools def _raw_gen( self, @@ -23,13 +97,32 @@ class GoogleLLM(BaseLLM): model, messages, stream=False, - **kwargs - ): - import google.generativeai as genai - genai.configure(api_key=self.api_key) - model = genai.GenerativeModel(model, system_instruction=messages[0]["content"]) - response = model.generate_content(self._clean_messages_google(messages)) - return response.text + tools=None, + formatting="openai", + **kwargs, + ): + client = genai.Client(api_key=self.api_key) + if formatting == "openai": + messages = self._clean_messages_google(messages) + config = types.GenerateContentConfig() + if messages[0].role == "system": + config.system_instruction = messages[0].parts[0].text + messages = messages[1:] + + if tools: + cleaned_tools = self._clean_tools_format(tools) + config.tools = cleaned_tools + response = client.models.generate_content( + model=model, + contents=messages, + config=config, + ) + return response + else: + response = client.models.generate_content( + model=model, contents=messages, config=config + ) + return response.text def _raw_gen_stream( self, @@ -37,12 +130,30 @@ class GoogleLLM(BaseLLM): model, messages, stream=True, - **kwargs - ): - import google.generativeai as genai - genai.configure(api_key=self.api_key) - model = genai.GenerativeModel(model, system_instruction=messages[0]["content"]) - response = model.generate_content(self._clean_messages_google(messages), stream=True) - for line in response: - if line.text is not None: - yield line.text \ No newline at end of file + tools=None, + formatting="openai", + **kwargs, + ): + client = genai.Client(api_key=self.api_key) + if formatting == "openai": + messages = self._clean_messages_google(messages) + config = types.GenerateContentConfig() + if messages[0].role == "system": + config.system_instruction = messages[0].parts[0].text + messages = messages[1:] + + if tools: + cleaned_tools = self._clean_tools_format(tools) + config.tools = cleaned_tools + + response = client.models.generate_content_stream( + model=model, + contents=messages, + config=config, + ) + for chunk in response: + if chunk.text is not None: + yield chunk.text + + def _supports_tools(self): + return True diff --git a/application/llm/groq.py b/application/llm/groq.py index b5731a90..282d7f47 100644 --- a/application/llm/groq.py +++ b/application/llm/groq.py @@ -1,45 +1,32 @@ from application.llm.base import BaseLLM - +from openai import OpenAI class GroqLLM(BaseLLM): - def __init__(self, api_key=None, user_api_key=None, *args, **kwargs): - from openai import OpenAI - super().__init__(*args, **kwargs) self.client = OpenAI(api_key=api_key, base_url="https://api.groq.com/openai/v1") self.api_key = api_key self.user_api_key = user_api_key - def _raw_gen( - self, - baseself, - model, - messages, - stream=False, - **kwargs - ): - response = self.client.chat.completions.create( - model=model, messages=messages, stream=stream, **kwargs - ) - - return response.choices[0].message.content + def _raw_gen(self, baseself, model, messages, stream=False, tools=None, **kwargs): + if tools: + response = self.client.chat.completions.create( + model=model, messages=messages, stream=stream, tools=tools, **kwargs + ) + return response.choices[0] + else: + response = self.client.chat.completions.create( + model=model, messages=messages, stream=stream, **kwargs + ) + return response.choices[0].message.content def _raw_gen_stream( - self, - baseself, - model, - messages, - stream=True, - **kwargs - ): + self, baseself, model, messages, stream=True, tools=None, **kwargs + ): response = self.client.chat.completions.create( model=model, messages=messages, stream=stream, **kwargs ) - for line in response: - # import sys - # print(line.choices[0].delta.content, file=sys.stderr) if line.choices[0].delta.content is not None: yield line.choices[0].delta.content diff --git a/application/llm/openai.py b/application/llm/openai.py index f85de6ea..36861584 100644 --- a/application/llm/openai.py +++ b/application/llm/openai.py @@ -1,6 +1,7 @@ -from application.llm.base import BaseLLM -from application.core.settings import settings +import json +from application.core.settings import settings +from application.llm.base import BaseLLM class OpenAILLM(BaseLLM): @@ -10,29 +11,95 @@ class OpenAILLM(BaseLLM): super().__init__(*args, **kwargs) if settings.OPENAI_BASE_URL: - self.client = OpenAI( - api_key=api_key, - base_url=settings.OPENAI_BASE_URL - ) + self.client = OpenAI(api_key=api_key, base_url=settings.OPENAI_BASE_URL) else: self.client = OpenAI(api_key=api_key) self.api_key = api_key self.user_api_key = user_api_key + def _clean_messages_openai(self, messages): + cleaned_messages = [] + for message in messages: + role = message.get("role") + content = message.get("content") + + if role == "model": + role = "assistant" + + if role and content is not None: + if isinstance(content, str): + cleaned_messages.append({"role": role, "content": content}) + elif isinstance(content, list): + for item in content: + if "text" in item: + cleaned_messages.append( + {"role": role, "content": item["text"]} + ) + elif "function_call" in item: + tool_call = { + "id": item["function_call"]["call_id"], + "type": "function", + "function": { + "name": item["function_call"]["name"], + "arguments": json.dumps( + item["function_call"]["args"] + ), + }, + } + cleaned_messages.append( + { + "role": "assistant", + "content": None, + "tool_calls": [tool_call], + } + ) + elif "function_response" in item: + cleaned_messages.append( + { + "role": "tool", + "tool_call_id": item["function_response"][ + "call_id" + ], + "content": json.dumps( + item["function_response"]["response"]["result"] + ), + } + ) + else: + raise ValueError( + f"Unexpected content dictionary format: {item}" + ) + else: + raise ValueError(f"Unexpected content type: {type(content)}") + + return cleaned_messages + def _raw_gen( self, baseself, model, messages, stream=False, + tools=None, engine=settings.AZURE_DEPLOYMENT_NAME, - **kwargs - ): - response = self.client.chat.completions.create( - model=model, messages=messages, stream=stream, **kwargs - ) - - return response.choices[0].message.content + **kwargs, + ): + messages = self._clean_messages_openai(messages) + print(messages) + if tools: + response = self.client.chat.completions.create( + model=model, + messages=messages, + stream=stream, + tools=tools, + **kwargs, + ) + return response.choices[0] + else: + response = self.client.chat.completions.create( + model=model, messages=messages, stream=stream, **kwargs + ) + return response.choices[0].message.content def _raw_gen_stream( self, @@ -40,19 +107,22 @@ class OpenAILLM(BaseLLM): model, messages, stream=True, + tools=None, engine=settings.AZURE_DEPLOYMENT_NAME, - **kwargs - ): + **kwargs, + ): + messages = self._clean_messages_openai(messages) response = self.client.chat.completions.create( model=model, messages=messages, stream=stream, **kwargs ) for line in response: - # import sys - # print(line.choices[0].delta.content, file=sys.stderr) if line.choices[0].delta.content is not None: yield line.choices[0].delta.content + def _supports_tools(self): + return True + class AzureOpenAILLM(OpenAILLM): diff --git a/application/llm/sagemaker.py b/application/llm/sagemaker.py index 63947430..aaf99a12 100644 --- a/application/llm/sagemaker.py +++ b/application/llm/sagemaker.py @@ -76,7 +76,7 @@ class SagemakerAPILLM(BaseLLM): self.endpoint = settings.SAGEMAKER_ENDPOINT self.runtime = runtime - def _raw_gen(self, baseself, model, messages, stream=False, **kwargs): + def _raw_gen(self, baseself, model, messages, stream=False, tools=None, **kwargs): context = messages[0]["content"] user_question = messages[-1]["content"] prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n" @@ -105,7 +105,7 @@ class SagemakerAPILLM(BaseLLM): print(result[0]["generated_text"], file=sys.stderr) return result[0]["generated_text"][len(prompt) :] - def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs): + def _raw_gen_stream(self, baseself, model, messages, stream=True, tools=None, **kwargs): context = messages[0]["content"] user_question = messages[-1]["content"] prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n" diff --git a/application/parser/chunking.py b/application/parser/chunking.py new file mode 100644 index 00000000..aae14898 --- /dev/null +++ b/application/parser/chunking.py @@ -0,0 +1,118 @@ +import re +from typing import List, Tuple +import logging +from application.parser.schema.base import Document +from application.utils import get_encoding + +logger = logging.getLogger(__name__) + +class Chunker: + def __init__( + self, + chunking_strategy: str = "classic_chunk", + max_tokens: int = 2000, + min_tokens: int = 150, + duplicate_headers: bool = False, + ): + if chunking_strategy not in ["classic_chunk"]: + raise ValueError(f"Unsupported chunking strategy: {chunking_strategy}") + self.chunking_strategy = chunking_strategy + self.max_tokens = max_tokens + self.min_tokens = min_tokens + self.duplicate_headers = duplicate_headers + self.encoding = get_encoding() + + def separate_header_and_body(self, text: str) -> Tuple[str, str]: + header_pattern = r"^(.*?\n){3}" + match = re.match(header_pattern, text) + if match: + header = match.group(0) + body = text[len(header):] + else: + header, body = "", text # No header, treat entire text as body + return header, body + + def combine_documents(self, doc: Document, next_doc: Document) -> Document: + combined_text = doc.text + " " + next_doc.text + combined_token_count = len(self.encoding.encode(combined_text)) + new_doc = Document( + text=combined_text, + doc_id=doc.doc_id, + embedding=doc.embedding, + extra_info={**(doc.extra_info or {}), "token_count": combined_token_count} + ) + return new_doc + + def split_document(self, doc: Document) -> List[Document]: + split_docs = [] + header, body = self.separate_header_and_body(doc.text) + header_tokens = self.encoding.encode(header) if header else [] + body_tokens = self.encoding.encode(body) + + current_position = 0 + part_index = 0 + while current_position < len(body_tokens): + end_position = current_position + self.max_tokens - len(header_tokens) + chunk_tokens = (header_tokens + body_tokens[current_position:end_position] + if self.duplicate_headers or part_index == 0 else body_tokens[current_position:end_position]) + chunk_text = self.encoding.decode(chunk_tokens) + new_doc = Document( + text=chunk_text, + doc_id=f"{doc.doc_id}-{part_index}", + embedding=doc.embedding, + extra_info={**(doc.extra_info or {}), "token_count": len(chunk_tokens)} + ) + split_docs.append(new_doc) + current_position = end_position + part_index += 1 + header_tokens = [] + return split_docs + + def classic_chunk(self, documents: List[Document]) -> List[Document]: + processed_docs = [] + i = 0 + while i < len(documents): + doc = documents[i] + tokens = self.encoding.encode(doc.text) + token_count = len(tokens) + + if self.min_tokens <= token_count <= self.max_tokens: + doc.extra_info = doc.extra_info or {} + doc.extra_info["token_count"] = token_count + processed_docs.append(doc) + i += 1 + elif token_count < self.min_tokens: + if i + 1 < len(documents): + next_doc = documents[i + 1] + next_tokens = self.encoding.encode(next_doc.text) + if token_count + len(next_tokens) <= self.max_tokens: + # Combine small documents + combined_doc = self.combine_documents(doc, next_doc) + processed_docs.append(combined_doc) + i += 2 + else: + # Keep the small document as is if adding next_doc would exceed max_tokens + doc.extra_info = doc.extra_info or {} + doc.extra_info["token_count"] = token_count + processed_docs.append(doc) + i += 1 + else: + # No next document to combine with; add the small document as is + doc.extra_info = doc.extra_info or {} + doc.extra_info["token_count"] = token_count + processed_docs.append(doc) + i += 1 + else: + # Split large documents + processed_docs.extend(self.split_document(doc)) + i += 1 + return processed_docs + + def chunk( + self, + documents: List[Document] + ) -> List[Document]: + if self.chunking_strategy == "classic_chunk": + return self.classic_chunk(documents) + else: + raise ValueError("Unsupported chunking strategy") diff --git a/application/parser/embedding_pipeline.py b/application/parser/embedding_pipeline.py new file mode 100755 index 00000000..0435cd14 --- /dev/null +++ b/application/parser/embedding_pipeline.py @@ -0,0 +1,86 @@ +import os +import logging +from retry import retry +from tqdm import tqdm +from application.core.settings import settings +from application.vectorstore.vector_creator import VectorCreator + + +@retry(tries=10, delay=60) +def add_text_to_store_with_retry(store, doc, source_id): + """ + Add a document's text and metadata to the vector store with retry logic. + Args: + store: The vector store object. + doc: The document to be added. + source_id: Unique identifier for the source. + """ + try: + doc.metadata["source_id"] = str(source_id) + store.add_texts([doc.page_content], metadatas=[doc.metadata]) + except Exception as e: + logging.error(f"Failed to add document with retry: {e}") + raise + + +def embed_and_store_documents(docs, folder_name, source_id, task_status): + """ + Embeds documents and stores them in a vector store. + + Args: + docs (list): List of documents to be embedded and stored. + folder_name (str): Directory to save the vector store. + source_id (str): Unique identifier for the source. + task_status: Task state manager for progress updates. + + Returns: + None + """ + # Ensure the folder exists + if not os.path.exists(folder_name): + os.makedirs(folder_name) + + # Initialize vector store + if settings.VECTOR_STORE == "faiss": + docs_init = [docs.pop(0)] + store = VectorCreator.create_vectorstore( + settings.VECTOR_STORE, + docs_init=docs_init, + source_id=folder_name, + embeddings_key=os.getenv("EMBEDDINGS_KEY"), + ) + else: + store = VectorCreator.create_vectorstore( + settings.VECTOR_STORE, + source_id=source_id, + embeddings_key=os.getenv("EMBEDDINGS_KEY"), + ) + store.delete_index() + + total_docs = len(docs) + + # Process and embed documents + for idx, doc in tqdm( + enumerate(docs), + desc="Embedding 🦖", + unit="docs", + total=total_docs, + bar_format="{l_bar}{bar}| Time Left: {remaining}", + ): + try: + # Update task status for progress tracking + progress = int(((idx + 1) / total_docs) * 100) + task_status.update_state(state="PROGRESS", meta={"current": progress}) + + # Add document to vector store + add_text_to_store_with_retry(store, doc, source_id) + except Exception as e: + logging.error(f"Error embedding document {idx}: {e}") + logging.info(f"Saving progress at document {idx} out of {total_docs}") + store.save_local(folder_name) + break + + # Save the vector store + if settings.VECTOR_STORE == "faiss": + store.save_local(folder_name) + logging.info("Vector store saved successfully.") diff --git a/application/parser/file/bulk.py b/application/parser/file/bulk.py index 3b8fbca8..8201b3f2 100644 --- a/application/parser/file/bulk.py +++ b/application/parser/file/bulk.py @@ -13,6 +13,7 @@ from application.parser.file.rst_parser import RstParser from application.parser.file.tabular_parser import PandasCSVParser,ExcelParser from application.parser.file.json_parser import JSONParser from application.parser.file.pptx_parser import PPTXParser +from application.parser.file.image_parser import ImageParser from application.parser.schema.base import Document DEFAULT_FILE_EXTRACTOR: Dict[str, BaseParser] = { @@ -27,6 +28,9 @@ DEFAULT_FILE_EXTRACTOR: Dict[str, BaseParser] = { ".mdx": MarkdownParser(), ".json":JSONParser(), ".pptx":PPTXParser(), + ".png": ImageParser(), + ".jpg": ImageParser(), + ".jpeg": ImageParser(), } diff --git a/application/parser/file/docs_parser.py b/application/parser/file/docs_parser.py index 861e8e58..a1295290 100644 --- a/application/parser/file/docs_parser.py +++ b/application/parser/file/docs_parser.py @@ -7,7 +7,8 @@ from pathlib import Path from typing import Dict from application.parser.file.base_parser import BaseParser - +from application.core.settings import settings +import requests class PDFParser(BaseParser): """PDF parser.""" @@ -18,22 +19,32 @@ class PDFParser(BaseParser): def parse_file(self, file: Path, errors: str = "ignore") -> str: """Parse file.""" + if settings.PARSE_PDF_AS_IMAGE: + doc2md_service = "https://llm.arc53.com/doc2md" + # alternatively you can use local vision capable LLM + with open(file, "rb") as file_loaded: + files = {'file': file_loaded} + response = requests.post(doc2md_service, files=files) + data = response.json()["markdown"] + return data + try: - import PyPDF2 + from pypdf import PdfReader except ImportError: - raise ValueError("PyPDF2 is required to read PDF files.") + raise ValueError("pypdf is required to read PDF files.") text_list = [] with open(file, "rb") as fp: # Create a PDF object - pdf = PyPDF2.PdfReader(fp) + pdf = PdfReader(fp) # Get the number of pages in the PDF document num_pages = len(pdf.pages) # Iterate over every page - for page in range(num_pages): + for page_index in range(num_pages): # Extract the text from the page - page_text = pdf.pages[page].extract_text() + page = pdf.pages[page_index] + page_text = page.extract_text() text_list.append(page_text) text = "\n".join(text_list) @@ -56,4 +67,4 @@ class DocxParser(BaseParser): text = docx2txt.process(file) - return text + return text \ No newline at end of file diff --git a/application/parser/file/image_parser.py b/application/parser/file/image_parser.py new file mode 100644 index 00000000..fd800d91 --- /dev/null +++ b/application/parser/file/image_parser.py @@ -0,0 +1,27 @@ +"""Image parser. + +Contains parser for .png, .jpg, .jpeg files. + +""" +from pathlib import Path +import requests +from typing import Dict, Union + +from application.parser.file.base_parser import BaseParser + + +class ImageParser(BaseParser): + """Image parser.""" + + def _init_parser(self) -> Dict: + """Init parser.""" + return {} + + def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, list[str]]: + doc2md_service = "https://llm.arc53.com/doc2md" + # alternatively you can use local vision capable LLM + with open(file, "rb") as file_loaded: + files = {'file': file_loaded} + response = requests.post(doc2md_service, files=files) + data = response.json()["markdown"] + return data diff --git a/application/parser/file/rst_parser.py b/application/parser/file/rst_parser.py index 633ec844..d39a0837 100644 --- a/application/parser/file/rst_parser.py +++ b/application/parser/file/rst_parser.py @@ -91,6 +91,25 @@ class RstParser(BaseParser): ] return rst_tups + def chunk_by_token_count(self, text: str, max_tokens: int = 100) -> List[str]: + """Chunk text by token count.""" + + avg_token_length = 5 + + chunk_size = max_tokens * avg_token_length + + chunks = [] + for i in range(0, len(text), chunk_size): + chunk = text[i:i+chunk_size] + if i + chunk_size < len(text): + last_space = chunk.rfind(' ') + if last_space != -1: + chunk = chunk[:last_space] + + chunks.append(chunk.strip()) + + return chunks + def remove_images(self, content: str) -> str: pattern = r"\.\. image:: (.*)" content = re.sub(pattern, "", content) @@ -136,7 +155,7 @@ class RstParser(BaseParser): return {} def parse_tups( - self, filepath: Path, errors: str = "ignore" + self, filepath: Path, errors: str = "ignore",max_tokens: Optional[int] = 1000 ) -> List[Tuple[Optional[str], str]]: """Parse file into tuples.""" with open(filepath, "r") as f: @@ -156,6 +175,15 @@ class RstParser(BaseParser): rst_tups = self.remove_whitespaces_excess(rst_tups) if self._remove_characters_excess: rst_tups = self.remove_characters_excess(rst_tups) + + # Apply chunking if max_tokens is provided + if max_tokens is not None: + chunked_tups = [] + for header, text in rst_tups: + chunks = self.chunk_by_token_count(text, max_tokens) + for idx, chunk in enumerate(chunks): + chunked_tups.append((f"{header} - Chunk {idx + 1}", chunk)) + return chunked_tups return rst_tups def parse_file( diff --git a/application/parser/java2doc.py b/application/parser/java2doc.py deleted file mode 100644 index 2a8bfa3a..00000000 --- a/application/parser/java2doc.py +++ /dev/null @@ -1,66 +0,0 @@ -import os - -import javalang - - -def find_files(directory): - files_list = [] - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith('.java'): - files_list.append(os.path.join(root, file)) - return files_list - - -def extract_functions(file_path): - with open(file_path, "r") as file: - java_code = file.read() - methods = {} - tree = javalang.parse.parse(java_code) - for _, node in tree.filter(javalang.tree.MethodDeclaration): - method_name = node.name - start_line = node.position.line - 1 - end_line = start_line - brace_count = 0 - for line in java_code.splitlines()[start_line:]: - end_line += 1 - brace_count += line.count("{") - line.count("}") - if brace_count == 0: - break - method_source_code = "\n".join(java_code.splitlines()[start_line:end_line]) - methods[method_name] = method_source_code - return methods - - -def extract_classes(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - classes = {} - tree = javalang.parse.parse(source_code) - for class_decl in tree.types: - class_name = class_decl.name - declarations = [] - methods = [] - for field_decl in class_decl.fields: - field_name = field_decl.declarators[0].name - field_type = field_decl.type.name - declarations.append(f"{field_type} {field_name}") - for method_decl in class_decl.methods: - methods.append(method_decl.name) - class_string = "Declarations: " + ", ".join(declarations) + "\n Method name: " + ", ".join(methods) - classes[class_name] = class_string - return classes - - -def extract_functions_and_classes(directory): - files = find_files(directory) - functions_dict = {} - classes_dict = {} - for file in files: - functions = extract_functions(file) - if functions: - functions_dict[file] = functions - classes = extract_classes(file) - if classes: - classes_dict[file] = classes - return functions_dict, classes_dict diff --git a/application/parser/js2doc.py b/application/parser/js2doc.py deleted file mode 100644 index 6dc44812..00000000 --- a/application/parser/js2doc.py +++ /dev/null @@ -1,70 +0,0 @@ -import os - -import escodegen -import esprima - - -def find_files(directory): - files_list = [] - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith('.js'): - files_list.append(os.path.join(root, file)) - return files_list - - -def extract_functions(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - functions = {} - tree = esprima.parseScript(source_code) - for node in tree.body: - if node.type == 'FunctionDeclaration': - func_name = node.id.name if node.id else '' - functions[func_name] = escodegen.generate(node) - elif node.type == 'VariableDeclaration': - for declaration in node.declarations: - if declaration.init and declaration.init.type == 'FunctionExpression': - func_name = declaration.id.name if declaration.id else '' - functions[func_name] = escodegen.generate(declaration.init) - elif node.type == 'ClassDeclaration': - for subnode in node.body.body: - if subnode.type == 'MethodDefinition': - func_name = subnode.key.name - functions[func_name] = escodegen.generate(subnode.value) - elif subnode.type == 'VariableDeclaration': - for declaration in subnode.declarations: - if declaration.init and declaration.init.type == 'FunctionExpression': - func_name = declaration.id.name if declaration.id else '' - functions[func_name] = escodegen.generate(declaration.init) - return functions - - -def extract_classes(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - classes = {} - tree = esprima.parseScript(source_code) - for node in tree.body: - if node.type == 'ClassDeclaration': - class_name = node.id.name - function_names = [] - for subnode in node.body.body: - if subnode.type == 'MethodDefinition': - function_names.append(subnode.key.name) - classes[class_name] = ", ".join(function_names) - return classes - - -def extract_functions_and_classes(directory): - files = find_files(directory) - functions_dict = {} - classes_dict = {} - for file in files: - functions = extract_functions(file) - if functions: - functions_dict[file] = functions - classes = extract_classes(file) - if classes: - classes_dict[file] = classes - return functions_dict, classes_dict diff --git a/application/parser/open_ai_func.py b/application/parser/open_ai_func.py deleted file mode 100755 index 3109f583..00000000 --- a/application/parser/open_ai_func.py +++ /dev/null @@ -1,75 +0,0 @@ -import os - -from retry import retry - -from application.core.settings import settings - -from application.vectorstore.vector_creator import VectorCreator - - -# from langchain_community.embeddings import HuggingFaceEmbeddings -# from langchain_community.embeddings import HuggingFaceInstructEmbeddings -# from langchain_community.embeddings import CohereEmbeddings - - -@retry(tries=10, delay=60) -def store_add_texts_with_retry(store, i, id): - # add source_id to the metadata - i.metadata["source_id"] = str(id) - store.add_texts([i.page_content], metadatas=[i.metadata]) - # store_pine.add_texts([i.page_content], metadatas=[i.metadata]) - - -def call_openai_api(docs, folder_name, id, task_status): - # Function to create a vector store from the documents and save it to disk - - if not os.path.exists(f"{folder_name}"): - os.makedirs(f"{folder_name}") - - from tqdm import tqdm - - c1 = 0 - if settings.VECTOR_STORE == "faiss": - docs_init = [docs[0]] - docs.pop(0) - - store = VectorCreator.create_vectorstore( - settings.VECTOR_STORE, - docs_init=docs_init, - source_id=f"{folder_name}", - embeddings_key=os.getenv("EMBEDDINGS_KEY"), - ) - else: - store = VectorCreator.create_vectorstore( - settings.VECTOR_STORE, - source_id=str(id), - embeddings_key=os.getenv("EMBEDDINGS_KEY"), - ) - store.delete_index() - # Uncomment for MPNet embeddings - # model_name = "sentence-transformers/all-mpnet-base-v2" - # hf = HuggingFaceEmbeddings(model_name=model_name) - # store = FAISS.from_documents(docs_test, hf) - s1 = len(docs) - for i in tqdm( - docs, - desc="Embedding 🦖", - unit="docs", - total=len(docs), - bar_format="{l_bar}{bar}| Time Left: {remaining}", - ): - try: - task_status.update_state( - state="PROGRESS", meta={"current": int((c1 / s1) * 100)} - ) - store_add_texts_with_retry(store, i, id) - except Exception as e: - print(e) - print("Error on ", i) - print("Saving progress") - print(f"stopped at {c1} out of {len(docs)}") - store.save_local(f"{folder_name}") - break - c1 += 1 - if settings.VECTOR_STORE == "faiss": - store.save_local(f"{folder_name}") diff --git a/application/parser/py2doc.py b/application/parser/py2doc.py deleted file mode 100644 index 3a8175d4..00000000 --- a/application/parser/py2doc.py +++ /dev/null @@ -1,121 +0,0 @@ -import ast -import os -from pathlib import Path - -import tiktoken -from langchain.llms import OpenAI -from langchain.prompts import PromptTemplate - - -def find_files(directory): - files_list = [] - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith('.py'): - files_list.append(os.path.join(root, file)) - return files_list - - -def extract_functions(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - functions = {} - tree = ast.parse(source_code) - for node in ast.walk(tree): - if isinstance(node, ast.FunctionDef): - func_name = node.name - func_def = ast.get_source_segment(source_code, node) - functions[func_name] = func_def - return functions - - -def extract_classes(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - classes = {} - tree = ast.parse(source_code) - for node in ast.walk(tree): - if isinstance(node, ast.ClassDef): - class_name = node.name - function_names = [] - for subnode in ast.walk(node): - if isinstance(subnode, ast.FunctionDef): - function_names.append(subnode.name) - classes[class_name] = ", ".join(function_names) - return classes - - -def extract_functions_and_classes(directory): - files = find_files(directory) - functions_dict = {} - classes_dict = {} - for file in files: - functions = extract_functions(file) - if functions: - functions_dict[file] = functions - classes = extract_classes(file) - if classes: - classes_dict[file] = classes - return functions_dict, classes_dict - - -def parse_functions(functions_dict, formats, dir): - c1 = len(functions_dict) - for i, (source, functions) in enumerate(functions_dict.items(), start=1): - print(f"Processing file {i}/{c1}") - source_w = source.replace(dir + "/", "").replace("." + formats, ".md") - subfolders = "/".join(source_w.split("/")[:-1]) - Path(f"outputs/{subfolders}").mkdir(parents=True, exist_ok=True) - for j, (name, function) in enumerate(functions.items(), start=1): - print(f"Processing function {j}/{len(functions)}") - prompt = PromptTemplate( - input_variables=["code"], - template="Code: \n{code}, \nDocumentation: ", - ) - llm = OpenAI(temperature=0) - response = llm(prompt.format(code=function)) - mode = "a" if Path(f"outputs/{source_w}").exists() else "w" - with open(f"outputs/{source_w}", mode) as f: - f.write( - f"\n\n# Function name: {name} \n\nFunction: \n```\n{function}\n```, \nDocumentation: \n{response}") - - -def parse_classes(classes_dict, formats, dir): - c1 = len(classes_dict) - for i, (source, classes) in enumerate(classes_dict.items()): - print(f"Processing file {i + 1}/{c1}") - source_w = source.replace(dir + "/", "").replace("." + formats, ".md") - subfolders = "/".join(source_w.split("/")[:-1]) - Path(f"outputs/{subfolders}").mkdir(parents=True, exist_ok=True) - for name, function_names in classes.items(): - print(f"Processing Class {i + 1}/{c1}") - prompt = PromptTemplate( - input_variables=["class_name", "functions_names"], - template="Class name: {class_name} \nFunctions: {functions_names}, \nDocumentation: ", - ) - llm = OpenAI(temperature=0) - response = llm(prompt.format(class_name=name, functions_names=function_names)) - - with open(f"outputs/{source_w}", "a" if Path(f"outputs/{source_w}").exists() else "w") as f: - f.write(f"\n\n# Class name: {name} \n\nFunctions: \n{function_names}, \nDocumentation: \n{response}") - - -def transform_to_docs(functions_dict, classes_dict, formats, dir): - docs_content = ''.join([str(key) + str(value) for key, value in functions_dict.items()]) - docs_content += ''.join([str(key) + str(value) for key, value in classes_dict.items()]) - - num_tokens = len(tiktoken.get_encoding("cl100k_base").encode(docs_content)) - total_price = ((num_tokens / 1000) * 0.02) - - print(f"Number of Tokens = {num_tokens:,d}") - print(f"Approx Cost = ${total_price:,.2f}") - - user_input = input("Price Okay? (Y/N)\n").lower() - if user_input == "y" or user_input == "": - if not Path("outputs").exists(): - Path("outputs").mkdir() - parse_functions(functions_dict, formats, dir) - parse_classes(classes_dict, formats, dir) - print("All done!") - else: - print("The API was not called. No money was spent.") diff --git a/application/parser/remote/crawler_loader.py b/application/parser/remote/crawler_loader.py index 76325ae6..c2da230b 100644 --- a/application/parser/remote/crawler_loader.py +++ b/application/parser/remote/crawler_loader.py @@ -2,16 +2,16 @@ import requests from urllib.parse import urlparse, urljoin from bs4 import BeautifulSoup from application.parser.remote.base import BaseRemote +from application.parser.schema.base import Document +from langchain_community.document_loaders import WebBaseLoader class CrawlerLoader(BaseRemote): def __init__(self, limit=10): - from langchain_community.document_loaders import WebBaseLoader self.loader = WebBaseLoader # Initialize the document loader self.limit = limit # Set the limit for the number of pages to scrape def load_data(self, inputs): url = inputs - # Check if the input is a list and if it is, use the first element if isinstance(url, list) and url: url = url[0] @@ -19,24 +19,29 @@ class CrawlerLoader(BaseRemote): if not urlparse(url).scheme: url = "http://" + url - visited_urls = set() # Keep track of URLs that have been visited - base_url = urlparse(url).scheme + "://" + urlparse(url).hostname # Extract the base URL - urls_to_visit = [url] # List of URLs to be visited, starting with the initial URL - loaded_content = [] # Store the loaded content from each URL + visited_urls = set() + base_url = urlparse(url).scheme + "://" + urlparse(url).hostname + urls_to_visit = [url] + loaded_content = [] - # Continue crawling until there are no more URLs to visit while urls_to_visit: - current_url = urls_to_visit.pop(0) # Get the next URL to visit - visited_urls.add(current_url) # Mark the URL as visited + current_url = urls_to_visit.pop(0) + visited_urls.add(current_url) - # Try to load and process the content from the current URL try: - response = requests.get(current_url) # Fetch the content of the current URL - response.raise_for_status() # Raise an exception for HTTP errors - loader = self.loader([current_url]) # Initialize the document loader for the current URL - loaded_content.extend(loader.load()) # Load the content and add it to the loaded_content list + response = requests.get(current_url) + response.raise_for_status() + loader = self.loader([current_url]) + docs = loader.load() + # Convert the loaded documents to your Document schema + for doc in docs: + loaded_content.append( + Document( + doc.page_content, + extra_info=doc.metadata + ) + ) except Exception as e: - # Print an error message if loading or processing fails and continue with the next URL print(f"Error processing URL {current_url}: {e}") continue @@ -45,15 +50,15 @@ class CrawlerLoader(BaseRemote): all_links = [ urljoin(current_url, a['href']) for a in soup.find_all('a', href=True) - if base_url in urljoin(current_url, a['href']) # Ensure links are from the same domain + if base_url in urljoin(current_url, a['href']) ] # Add new links to the list of URLs to visit if they haven't been visited yet urls_to_visit.extend([link for link in all_links if link not in visited_urls]) - urls_to_visit = list(set(urls_to_visit)) # Remove duplicate URLs + urls_to_visit = list(set(urls_to_visit)) # Stop crawling if the limit of pages to scrape is reached if self.limit is not None and len(visited_urls) >= self.limit: break - return loaded_content # Return the loaded content from all visited URLs + return loaded_content \ No newline at end of file diff --git a/application/parser/remote/crawler_markdown.py b/application/parser/remote/crawler_markdown.py new file mode 100644 index 00000000..3d199332 --- /dev/null +++ b/application/parser/remote/crawler_markdown.py @@ -0,0 +1,139 @@ +import requests +from urllib.parse import urlparse, urljoin +from bs4 import BeautifulSoup +from application.parser.remote.base import BaseRemote +import re +from markdownify import markdownify +from application.parser.schema.base import Document +import tldextract + +class CrawlerLoader(BaseRemote): + def __init__(self, limit=10, allow_subdomains=False): + """ + Given a URL crawl web pages up to `self.limit`, + convert HTML content to Markdown, and returning a list of Document objects. + + :param limit: The maximum number of pages to crawl. + :param allow_subdomains: If True, crawl pages on subdomains of the base domain. + """ + self.limit = limit + self.allow_subdomains = allow_subdomains + self.session = requests.Session() + + def load_data(self, inputs): + url = inputs + if isinstance(url, list) and url: + url = url[0] + + # Ensure the URL has a scheme (if not, default to http) + if not urlparse(url).scheme: + url = "http://" + url + + # Keep track of visited URLs to avoid revisiting the same page + visited_urls = set() + + # Determine the base domain for link filtering using tldextract + base_domain = self._get_base_domain(url) + urls_to_visit = {url} + documents = [] + + while urls_to_visit: + current_url = urls_to_visit.pop() + + # Skip if already visited + if current_url in visited_urls: + continue + visited_urls.add(current_url) + + # Fetch the page content + html_content = self._fetch_page(current_url) + if html_content is None: + continue + + # Convert the HTML to Markdown for cleaner text formatting + title, language, processed_markdown = self._process_html_to_markdown(html_content, current_url) + if processed_markdown: + # Create a Document for each visited page + documents.append( + Document( + processed_markdown, # content + None, # doc_id + None, # embedding + {"source": current_url, "title": title, "language": language} # extra_info + ) + ) + + # Extract links and filter them according to domain rules + new_links = self._extract_links(html_content, current_url) + filtered_links = self._filter_links(new_links, base_domain) + + # Add any new, not-yet-visited links to the queue + urls_to_visit.update(link for link in filtered_links if link not in visited_urls) + + # If we've reached the limit, stop crawling + if self.limit is not None and len(visited_urls) >= self.limit: + break + + return documents + + def _fetch_page(self, url): + try: + response = self.session.get(url, timeout=10) + response.raise_for_status() + return response.text + except requests.exceptions.RequestException as e: + print(f"Error fetching URL {url}: {e}") + return None + + def _process_html_to_markdown(self, html_content, current_url): + soup = BeautifulSoup(html_content, 'html.parser') + title_tag = soup.find('title') + title = title_tag.text.strip() if title_tag else "No Title" + + # Extract language + language_tag = soup.find('html') + language = language_tag.get('lang', 'en') if language_tag else "en" + + markdownified = markdownify(html_content, heading_style="ATX", newline_style="BACKSLASH") + # Reduce sequences of more than two newlines to exactly three + markdownified = re.sub(r'\n{3,}', '\n\n\n', markdownified) + return title, language, markdownified + + def _extract_links(self, html_content, current_url): + soup = BeautifulSoup(html_content, 'html.parser') + links = [] + for a in soup.find_all('a', href=True): + full_url = urljoin(current_url, a['href']) + links.append((full_url, a.text.strip())) + return links + + def _get_base_domain(self, url): + extracted = tldextract.extract(url) + # Reconstruct the domain as domain.suffix + base_domain = f"{extracted.domain}.{extracted.suffix}" + return base_domain + + def _filter_links(self, links, base_domain): + """ + Filter the extracted links to only include those that match the crawling criteria: + - If allow_subdomains is True, allow any link whose domain ends with the base_domain. + - If allow_subdomains is False, only allow exact matches of the base_domain. + """ + filtered = [] + for link, _ in links: + parsed_link = urlparse(link) + if not parsed_link.netloc: + continue + + extracted = tldextract.extract(parsed_link.netloc) + link_base = f"{extracted.domain}.{extracted.suffix}" + + if self.allow_subdomains: + # For subdomains: sub.example.com ends with example.com + if link_base == base_domain or link_base.endswith("." + base_domain): + filtered.append(link) + else: + # Exact domain match + if link_base == base_domain: + filtered.append(link) + return filtered \ No newline at end of file diff --git a/application/parser/remote/web_loader.py b/application/parser/remote/web_loader.py index a19e0c90..cc1cdcb8 100644 --- a/application/parser/remote/web_loader.py +++ b/application/parser/remote/web_loader.py @@ -1,5 +1,7 @@ from application.parser.remote.base import BaseRemote +from application.parser.schema.base import Document from langchain_community.document_loaders import WebBaseLoader +from urllib.parse import urlparse headers = { "User-Agent": "Mozilla/5.0", @@ -23,10 +25,20 @@ class WebLoader(BaseRemote): urls = [urls] documents = [] for url in urls: + # Check if the URL scheme is provided, if not, assume http + if not urlparse(url).scheme: + url = "http://" + url try: loader = self.loader([url], header_template=headers) - documents.extend(loader.load()) + loaded_docs = loader.load() + for doc in loaded_docs: + documents.append( + Document( + doc.page_content, + extra_info=doc.metadata, + ) + ) except Exception as e: print(f"Error processing URL {url}: {e}") continue - return documents + return documents \ No newline at end of file diff --git a/application/parser/token_func.py b/application/parser/token_func.py deleted file mode 100644 index 7511cde0..00000000 --- a/application/parser/token_func.py +++ /dev/null @@ -1,79 +0,0 @@ -import re -from math import ceil -from typing import List - -import tiktoken -from application.parser.schema.base import Document - - -def separate_header_and_body(text): - header_pattern = r"^(.*?\n){3}" - match = re.match(header_pattern, text) - header = match.group(0) - body = text[len(header):] - return header, body - - -def group_documents(documents: List[Document], min_tokens: int, max_tokens: int) -> List[Document]: - docs = [] - current_group = None - - for doc in documents: - doc_len = len(tiktoken.get_encoding("cl100k_base").encode(doc.text)) - - # Check if current group is empty or if the document can be added based on token count and matching metadata - if (current_group is None or - (len(tiktoken.get_encoding("cl100k_base").encode(current_group.text)) + doc_len < max_tokens and - doc_len < min_tokens and - current_group.extra_info == doc.extra_info)): - if current_group is None: - current_group = doc # Use the document directly to retain its metadata - else: - current_group.text += " " + doc.text # Append text to the current group - else: - docs.append(current_group) - current_group = doc # Start a new group with the current document - - if current_group is not None: - docs.append(current_group) - - return docs - - -def split_documents(documents: List[Document], max_tokens: int) -> List[Document]: - docs = [] - for doc in documents: - token_length = len(tiktoken.get_encoding("cl100k_base").encode(doc.text)) - if token_length <= max_tokens: - docs.append(doc) - else: - header, body = separate_header_and_body(doc.text) - if len(tiktoken.get_encoding("cl100k_base").encode(header)) > max_tokens: - body = doc.text - header = "" - num_body_parts = ceil(token_length / max_tokens) - part_length = ceil(len(body) / num_body_parts) - body_parts = [body[i:i + part_length] for i in range(0, len(body), part_length)] - for i, body_part in enumerate(body_parts): - new_doc = Document(text=header + body_part.strip(), - doc_id=f"{doc.doc_id}-{i}", - embedding=doc.embedding, - extra_info=doc.extra_info) - docs.append(new_doc) - return docs - - -def group_split(documents: List[Document], max_tokens: int = 2000, min_tokens: int = 150, token_check: bool = True): - if not token_check: - return documents - print("Grouping small documents") - try: - documents = group_documents(documents=documents, min_tokens=min_tokens, max_tokens=max_tokens) - except Exception: - print("Grouping failed, try running without token_check") - print("Separating large documents") - try: - documents = split_documents(documents=documents, max_tokens=max_tokens) - except Exception: - print("Grouping failed, try running without token_check") - return documents diff --git a/application/requirements.txt b/application/requirements.txt index 2f28c2ea..1707ad80 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -1,25 +1,27 @@ -anthropic==0.34.2 -boto3==1.34.153 +anthropic==0.45.2 +boto3==1.35.97 beautifulsoup4==4.12.3 -celery==5.3.6 +celery==5.4.0 dataclasses-json==0.6.7 docx2txt==0.8 duckduckgo-search==6.3.0 ebooklib==0.18 -elastic-transport==8.15.0 -elasticsearch==8.15.1 +elastic-transport==8.17.0 +elasticsearch==8.17.0 escodegen==1.0.11 esprima==4.0.1 esutils==1.0.1 -Flask==3.0.3 -faiss-cpu==1.8.0.post1 +Flask==3.1.0 +faiss-cpu==1.9.0.post1 flask-restx==1.3.0 -gTTS==2.3.2 +google-genai==0.5.0 +google-generativeai==0.8.3 +gTTS==2.5.4 gunicorn==23.0.0 html2text==2024.2.26 javalang==0.13.0 -jinja2==3.1.4 -jiter==0.5.0 +jinja2==3.1.5 +jiter==0.8.2 jmespath==1.0.1 joblib==1.4.2 jsonpatch==1.33 @@ -28,62 +30,66 @@ jsonschema==4.23.0 jsonschema-spec==0.2.4 jsonschema-specifications==2023.7.1 kombu==5.4.2 -langchain==0.3.0 -langchain-community==0.3.0 -langchain-core==0.3.2 -langchain-openai==0.2.0 -langchain-text-splitters==0.3.0 -langsmith==0.1.125 +langchain==0.3.14 +langchain-community==0.3.14 +langchain-core==0.3.29 +langchain-openai==0.3.0 +langchain-text-splitters==0.3.5 +langsmith==0.2.10 lazy-object-proxy==1.10.0 lxml==5.3.0 -markupsafe==2.1.5 -marshmallow==3.22.0 +markupsafe==3.0.2 +marshmallow==3.26.1 mpmath==1.3.0 multidict==6.1.0 mypy-extensions==1.0.0 -networkx==3.3 -numpy==1.26.4 -openai==1.46.1 +networkx==3.4.2 +numpy==2.2.1 +openai==1.59.5 openapi-schema-validator==0.6.2 openapi-spec-validator==0.6.0 -openapi3-parser==1.1.18 -orjson==3.10.7 +openapi3-parser==1.1.19 +orjson==3.10.14 packaging==24.1 pandas==2.2.3 openpyxl==3.1.5 -pathable==0.4.3 -pillow==10.4.0 +pathable==0.4.4 +pillow==11.1.0 portalocker==2.10.1 prance==23.6.21.0 -primp==0.6.3 -prompt-toolkit==3.0.47 -protobuf==5.28.2 +primp==0.10.0 +prompt-toolkit==3.0.50 +protobuf==5.29.3 +psycopg2-binary==2.9.10 py==1.11.0 -pydantic==2.9.2 -pydantic-core==2.23.4 -pydantic-settings==2.4.0 -pymongo==4.8.0 -pypdf2==3.0.1 +pydantic==2.10.4 +pydantic-core==2.27.2 +pydantic-settings==2.7.1 +pymongo==4.10.1 +pypdf==5.2.0 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 python-pptx==1.0.2 -qdrant-client==1.11.0 -redis==5.0.1 +qdrant-client==1.12.2 +redis==5.2.1 referencing==0.30.2 -regex==2024.9.11 +regex==2024.11.6 requests==2.32.3 retry==0.9.2 -sentence-transformers==3.0.1 -tiktoken==0.7.0 -tokenizers==0.19.1 -torch==2.4.1 -tqdm==4.66.5 -transformers==4.44.2 +sentence-transformers==3.3.1 +tiktoken==0.8.0 +tokenizers==0.21.0 +torch==2.5.1 +tqdm==4.67.1 +transformers==4.48.0 typing-extensions==4.12.2 typing-inspect==0.9.0 tzdata==2024.2 -urllib3==2.2.3 +urllib3==2.3.0 vine==5.1.0 wcwidth==0.2.13 -werkzeug==3.0.4 -yarl==1.11.1 \ No newline at end of file +werkzeug==3.1.3 +yarl==1.18.3 +markdownify==0.14.1 +tldextract==5.1.3 +websockets==14.1 diff --git a/application/retriever/brave_search.py b/application/retriever/brave_search.py index 1fd844b2..08b16bc0 100644 --- a/application/retriever/brave_search.py +++ b/application/retriever/brave_search.py @@ -2,7 +2,6 @@ import json from application.retriever.base import BaseRetriever from application.core.settings import settings from application.llm.llm_creator import LLMCreator -from application.utils import num_tokens_from_string from langchain_community.tools import BraveSearch @@ -72,22 +71,13 @@ class BraveRetSearch(BaseRetriever): for doc in docs: yield {"source": doc} - if len(self.chat_history) > 1: - tokens_current_history = 0 - # count tokens in history + if len(self.chat_history) > 0: for i in self.chat_history: if "prompt" in i and "response" in i: - tokens_batch = num_tokens_from_string(i["prompt"]) + num_tokens_from_string( - i["response"] + messages_combine.append({"role": "user", "content": i["prompt"]}) + messages_combine.append( + {"role": "assistant", "content": i["response"]} ) - if tokens_current_history + tokens_batch < self.token_limit: - tokens_current_history += tokens_batch - messages_combine.append( - {"role": "user", "content": i["prompt"]} - ) - messages_combine.append( - {"role": "system", "content": i["response"]} - ) messages_combine.append({"role": "user", "content": self.question}) llm = LLMCreator.create_llm( diff --git a/application/retriever/classic_rag.py b/application/retriever/classic_rag.py index 42e318d2..ca40f966 100644 --- a/application/retriever/classic_rag.py +++ b/application/retriever/classic_rag.py @@ -1,9 +1,10 @@ -from application.retriever.base import BaseRetriever -from application.core.settings import settings -from application.vectorstore.vector_creator import VectorCreator -from application.llm.llm_creator import LLMCreator +import uuid -from application.utils import num_tokens_from_string +from application.core.settings import settings +from application.retriever.base import BaseRetriever +from application.tools.agent import Agent + +from application.vectorstore.vector_creator import VectorCreator class ClassicRAG(BaseRetriever): @@ -20,7 +21,7 @@ class ClassicRAG(BaseRetriever): user_api_key=None, ): self.question = question - self.vectorstore = source['active_docs'] if 'active_docs' in source else None + self.vectorstore = source["active_docs"] if "active_docs" in source else None self.chat_history = chat_history self.prompt = prompt self.chunks = chunks @@ -36,6 +37,12 @@ class ClassicRAG(BaseRetriever): ) ) self.user_api_key = user_api_key + self.agent = Agent( + llm_name=settings.LLM_NAME, + gpt_model=self.gpt_model, + api_key=settings.API_KEY, + user_api_key=self.user_api_key, + ) def _get_data(self): if self.chunks == 0: @@ -72,34 +79,52 @@ class ClassicRAG(BaseRetriever): for doc in docs: yield {"source": doc} - if len(self.chat_history) > 1: - tokens_current_history = 0 - # count tokens in history + if len(self.chat_history) > 0: for i in self.chat_history: if "prompt" in i and "response" in i: - tokens_batch = num_tokens_from_string(i["prompt"]) + num_tokens_from_string( - i["response"] + messages_combine.append({"role": "user", "content": i["prompt"]}) + messages_combine.append( + {"role": "assistant", "content": i["response"]} ) - if tokens_current_history + tokens_batch < self.token_limit: - tokens_current_history += tokens_batch + if "tool_calls" in i: + for tool_call in i["tool_calls"]: + call_id = tool_call.get("call_id") + if call_id is None or call_id == "None": + call_id = str(uuid.uuid4()) + + function_call_dict = { + "function_call": { + "name": tool_call.get("action_name"), + "args": tool_call.get("arguments"), + "call_id": call_id, + } + } + function_response_dict = { + "function_response": { + "name": tool_call.get("action_name"), + "response": {"result": tool_call.get("result")}, + "call_id": call_id, + } + } + messages_combine.append( - {"role": "user", "content": i["prompt"]} + {"role": "assistant", "content": [function_call_dict]} ) messages_combine.append( - {"role": "system", "content": i["response"]} + {"role": "tool", "content": [function_response_dict]} ) + messages_combine.append({"role": "user", "content": self.question}) + completion = self.agent.gen(messages_combine) - llm = LLMCreator.create_llm( - settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=self.user_api_key - ) - completion = llm.gen_stream(model=self.gpt_model, messages=messages_combine) for line in completion: yield {"answer": str(line)} + yield {"tool_calls": self.agent.tool_calls.copy()} + def search(self): return self._get_data() - + def get_params(self): return { "question": self.question, @@ -109,5 +134,5 @@ class ClassicRAG(BaseRetriever): "chunks": self.chunks, "token_limit": self.token_limit, "gpt_model": self.gpt_model, - "user_api_key": self.user_api_key + "user_api_key": self.user_api_key, } diff --git a/application/retriever/duckduck_search.py b/application/retriever/duckduck_search.py index 6ae56226..c6386410 100644 --- a/application/retriever/duckduck_search.py +++ b/application/retriever/duckduck_search.py @@ -1,7 +1,6 @@ from application.retriever.base import BaseRetriever from application.core.settings import settings from application.llm.llm_creator import LLMCreator -from application.utils import num_tokens_from_string from langchain_community.tools import DuckDuckGoSearchResults from langchain_community.utilities import DuckDuckGoSearchAPIWrapper @@ -89,21 +88,12 @@ class DuckDuckSearch(BaseRetriever): for doc in docs: yield {"source": doc} - if len(self.chat_history) > 1: - tokens_current_history = 0 - # count tokens in history + if len(self.chat_history) > 0: for i in self.chat_history: - if "prompt" in i and "response" in i: - tokens_batch = num_tokens_from_string(i["prompt"]) + num_tokens_from_string( - i["response"] - ) - if tokens_current_history + tokens_batch < self.token_limit: - tokens_current_history += tokens_batch + if "prompt" in i and "response" in i: + messages_combine.append({"role": "user", "content": i["prompt"]}) messages_combine.append( - {"role": "user", "content": i["prompt"]} - ) - messages_combine.append( - {"role": "system", "content": i["response"]} + {"role": "assistant", "content": i["response"]} ) messages_combine.append({"role": "user", "content": self.question}) diff --git a/application/tools/agent.py b/application/tools/agent.py new file mode 100644 index 00000000..10798862 --- /dev/null +++ b/application/tools/agent.py @@ -0,0 +1,184 @@ +from application.core.mongo_db import MongoDB +from application.llm.llm_creator import LLMCreator +from application.tools.llm_handler import get_llm_handler +from application.tools.tool_action_parser import ToolActionParser +from application.tools.tool_manager import ToolManager + + +class Agent: + def __init__(self, llm_name, gpt_model, api_key, user_api_key=None): + # Initialize the LLM with the provided parameters + self.llm = LLMCreator.create_llm( + llm_name, api_key=api_key, user_api_key=user_api_key + ) + self.llm_handler = get_llm_handler(llm_name) + self.gpt_model = gpt_model + # Static tool configuration (to be replaced later) + self.tools = [] + self.tool_config = {} + self.tool_calls = [] + + def _get_user_tools(self, user="local"): + mongo = MongoDB.get_client() + db = mongo["docsgpt"] + user_tools_collection = db["user_tools"] + user_tools = user_tools_collection.find({"user": user, "status": True}) + user_tools = list(user_tools) + tools_by_id = {str(tool["_id"]): tool for tool in user_tools} + return tools_by_id + + def _build_tool_parameters(self, action): + params = {"type": "object", "properties": {}, "required": []} + for param_type in ["query_params", "headers", "body", "parameters"]: + if param_type in action and action[param_type].get("properties"): + for k, v in action[param_type]["properties"].items(): + if v.get("filled_by_llm", True): + params["properties"][k] = { + key: value + for key, value in v.items() + if key != "filled_by_llm" and key != "value" + } + + params["required"].append(k) + return params + + def _prepare_tools(self, tools_dict): + self.tools = [ + { + "type": "function", + "function": { + "name": f"{action['name']}_{tool_id}", + "description": action["description"], + "parameters": self._build_tool_parameters(action), + }, + } + for tool_id, tool in tools_dict.items() + if ( + (tool["name"] == "api_tool" and "actions" in tool.get("config", {})) + or (tool["name"] != "api_tool" and "actions" in tool) + ) + for action in ( + tool["config"]["actions"].values() + if tool["name"] == "api_tool" + else tool["actions"] + ) + if action.get("active", True) + ] + + def _execute_tool_action(self, tools_dict, call): + parser = ToolActionParser(self.llm.__class__.__name__) + tool_id, action_name, call_args = parser.parse_args(call) + + tool_data = tools_dict[tool_id] + action_data = ( + tool_data["config"]["actions"][action_name] + if tool_data["name"] == "api_tool" + else next( + action + for action in tool_data["actions"] + if action["name"] == action_name + ) + ) + + query_params, headers, body, parameters = {}, {}, {}, {} + param_types = { + "query_params": query_params, + "headers": headers, + "body": body, + "parameters": parameters, + } + + for param_type, target_dict in param_types.items(): + if param_type in action_data and action_data[param_type].get("properties"): + for param, details in action_data[param_type]["properties"].items(): + if param not in call_args and "value" in details: + target_dict[param] = details["value"] + + for param, value in call_args.items(): + for param_type, target_dict in param_types.items(): + if param_type in action_data and param in action_data[param_type].get( + "properties", {} + ): + target_dict[param] = value + + tm = ToolManager(config={}) + tool = tm.load_tool( + tool_data["name"], + tool_config=( + { + "url": tool_data["config"]["actions"][action_name]["url"], + "method": tool_data["config"]["actions"][action_name]["method"], + "headers": headers, + "query_params": query_params, + } + if tool_data["name"] == "api_tool" + else tool_data["config"] + ), + ) + if tool_data["name"] == "api_tool": + print( + f"Executing api: {action_name} with query_params: {query_params}, headers: {headers}, body: {body}" + ) + result = tool.execute_action(action_name, **body) + else: + print(f"Executing tool: {action_name} with args: {call_args}") + result = tool.execute_action(action_name, **parameters) + call_id = getattr(call, "id", None) + + tool_call_data = { + "tool_name": tool_data["name"], + "call_id": call_id if call_id is not None else "None", + "action_name": f"{action_name}_{tool_id}", + "arguments": call_args, + "result": result, + } + self.tool_calls.append(tool_call_data) + + return result, call_id + + def _simple_tool_agent(self, messages): + tools_dict = self._get_user_tools() + self._prepare_tools(tools_dict) + + resp = self.llm.gen(model=self.gpt_model, messages=messages, tools=self.tools) + + if isinstance(resp, str): + yield resp + return + if ( + hasattr(resp, "message") + and hasattr(resp.message, "content") + and resp.message.content is not None + ): + yield resp.message.content + return + + resp = self.llm_handler.handle_response(self, resp, tools_dict, messages) + + if isinstance(resp, str): + yield resp + elif ( + hasattr(resp, "message") + and hasattr(resp.message, "content") + and resp.message.content is not None + ): + yield resp.message.content + else: + completion = self.llm.gen_stream( + model=self.gpt_model, messages=messages, tools=self.tools + ) + for line in completion: + yield line + + return + + def gen(self, messages): + self.tool_calls = [] + if self.llm.supports_tools(): + resp = self._simple_tool_agent(messages) + for line in resp: + yield line + else: + resp = self.llm.gen_stream(model=self.gpt_model, messages=messages) + for line in resp: + yield line diff --git a/application/tools/base.py b/application/tools/base.py new file mode 100644 index 00000000..fd7b4a85 --- /dev/null +++ b/application/tools/base.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod + + +class Tool(ABC): + @abstractmethod + def execute_action(self, action_name: str, **kwargs): + pass + + @abstractmethod + def get_actions_metadata(self): + """ + Returns a list of JSON objects describing the actions supported by the tool. + """ + pass + + @abstractmethod + def get_config_requirements(self): + """ + Returns a dictionary describing the configuration requirements for the tool. + """ + pass diff --git a/application/tools/implementations/api_tool.py b/application/tools/implementations/api_tool.py new file mode 100644 index 00000000..5d0fec70 --- /dev/null +++ b/application/tools/implementations/api_tool.py @@ -0,0 +1,54 @@ +import json + +import requests +from application.tools.base import Tool + + +class APITool(Tool): + """ + API Tool + A flexible tool for performing various API actions (e.g., sending messages, retrieving data) via custom user-specified APIs + """ + + def __init__(self, config): + self.config = config + self.url = config.get("url", "") + self.method = config.get("method", "GET") + self.headers = config.get("headers", {"Content-Type": "application/json"}) + self.query_params = config.get("query_params", {}) + + def execute_action(self, action_name, **kwargs): + return self._make_api_call( + self.url, self.method, self.headers, self.query_params, kwargs + ) + + def _make_api_call(self, url, method, headers, query_params, body): + if query_params: + url = f"{url}?{requests.compat.urlencode(query_params)}" + if isinstance(body, dict): + body = json.dumps(body) + try: + print(f"Making API call: {method} {url} with body: {body}") + response = requests.request(method, url, headers=headers, data=body) + response.raise_for_status() + try: + data = response.json() + except ValueError: + data = None + + return { + "status_code": response.status_code, + "data": data, + "message": "API call successful.", + } + except requests.exceptions.RequestException as e: + return { + "status_code": response.status_code if response else None, + "message": f"API call failed: {str(e)}", + } + + def get_actions_metadata(self): + return [] + + def get_config_requirements(self): + return {} diff --git a/application/tools/implementations/cryptoprice.py b/application/tools/implementations/cryptoprice.py new file mode 100644 index 00000000..7b88c866 --- /dev/null +++ b/application/tools/implementations/cryptoprice.py @@ -0,0 +1,77 @@ +import requests +from application.tools.base import Tool + + +class CryptoPriceTool(Tool): + """ + CryptoPrice + A tool for retrieving cryptocurrency prices using the CryptoCompare public API + """ + + def __init__(self, config): + self.config = config + + def execute_action(self, action_name, **kwargs): + actions = {"cryptoprice_get": self._get_price} + + if action_name in actions: + return actions[action_name](**kwargs) + else: + raise ValueError(f"Unknown action: {action_name}") + + def _get_price(self, symbol, currency): + """ + Fetches the current price of a given cryptocurrency symbol in the specified currency. + Example: + symbol = "BTC" + currency = "USD" + returns price in USD. + """ + url = f"https://min-api.cryptocompare.com/data/price?fsym={symbol.upper()}&tsyms={currency.upper()}" + response = requests.get(url) + if response.status_code == 200: + data = response.json() + # data will be like {"USD": } if the call is successful + if currency.upper() in data: + return { + "status_code": response.status_code, + "price": data[currency.upper()], + "message": f"Price of {symbol.upper()} in {currency.upper()} retrieved successfully.", + } + else: + return { + "status_code": response.status_code, + "message": f"Couldn't find price for {symbol.upper()} in {currency.upper()}.", + } + else: + return { + "status_code": response.status_code, + "message": "Failed to retrieve price.", + } + + def get_actions_metadata(self): + return [ + { + "name": "cryptoprice_get", + "description": "Retrieve the price of a specified cryptocurrency in a given currency", + "parameters": { + "type": "object", + "properties": { + "symbol": { + "type": "string", + "description": "The cryptocurrency symbol (e.g. BTC)", + }, + "currency": { + "type": "string", + "description": "The currency in which you want the price (e.g. USD)", + }, + }, + "required": ["symbol", "currency"], + "additionalProperties": False, + }, + } + ] + + def get_config_requirements(self): + # No specific configuration needed for this tool as it just queries a public endpoint + return {} diff --git a/application/tools/implementations/postgres.py b/application/tools/implementations/postgres.py new file mode 100644 index 00000000..a83db9aa --- /dev/null +++ b/application/tools/implementations/postgres.py @@ -0,0 +1,163 @@ +import psycopg2 +from application.tools.base import Tool + +class PostgresTool(Tool): + """ + PostgreSQL Database Tool + A tool for connecting to a PostgreSQL database using a connection string, + executing SQL queries, and retrieving schema information. + """ + + def __init__(self, config): + self.config = config + self.connection_string = config.get("token", "") + + def execute_action(self, action_name, **kwargs): + actions = { + "postgres_execute_sql": self._execute_sql, + "postgres_get_schema": self._get_schema, + } + + if action_name in actions: + return actions[action_name](**kwargs) + else: + raise ValueError(f"Unknown action: {action_name}") + + def _execute_sql(self, sql_query): + """ + Executes an SQL query against the PostgreSQL database using a connection string. + """ + conn = None # Initialize conn to None for error handling + try: + conn = psycopg2.connect(self.connection_string) + cur = conn.cursor() + cur.execute(sql_query) + conn.commit() + + if sql_query.strip().lower().startswith("select"): + column_names = [desc[0] for desc in cur.description] if cur.description else [] + results = [] + rows = cur.fetchall() + for row in rows: + results.append(dict(zip(column_names, row))) + response_data = {"data": results, "column_names": column_names} + else: + row_count = cur.rowcount + response_data = {"message": f"Query executed successfully, {row_count} rows affected."} + + cur.close() + return { + "status_code": 200, + "message": "SQL query executed successfully.", + "response_data": response_data, + } + + except psycopg2.Error as e: + error_message = f"Database error: {e}" + print(f"Database error: {e}") + return { + "status_code": 500, + "message": "Failed to execute SQL query.", + "error": error_message, + } + finally: + if conn: # Ensure connection is closed even if errors occur + conn.close() + + def _get_schema(self, db_name): + """ + Retrieves the schema of the PostgreSQL database using a connection string. + """ + conn = None # Initialize conn to None for error handling + try: + conn = psycopg2.connect(self.connection_string) + cur = conn.cursor() + + cur.execute(""" + SELECT + table_name, + column_name, + data_type, + column_default, + is_nullable + FROM + information_schema.columns + WHERE + table_schema = 'public' + ORDER BY + table_name, + ordinal_position; + """) + + schema_data = {} + for row in cur.fetchall(): + table_name, column_name, data_type, column_default, is_nullable = row + if table_name not in schema_data: + schema_data[table_name] = [] + schema_data[table_name].append({ + "column_name": column_name, + "data_type": data_type, + "column_default": column_default, + "is_nullable": is_nullable + }) + + cur.close() + return { + "status_code": 200, + "message": "Database schema retrieved successfully.", + "schema": schema_data, + } + + except psycopg2.Error as e: + error_message = f"Database error: {e}" + print(f"Database error: {e}") + return { + "status_code": 500, + "message": "Failed to retrieve database schema.", + "error": error_message, + } + finally: + if conn: # Ensure connection is closed even if errors occur + conn.close() + + def get_actions_metadata(self): + return [ + { + "name": "postgres_execute_sql", + "description": "Execute an SQL query against the PostgreSQL database and return the results. Use this tool to interact with the database, e.g., retrieve specific data or perform updates. Only SELECT queries will return data, other queries will return execution status.", + "parameters": { + "type": "object", + "properties": { + "sql_query": { + "type": "string", + "description": "The SQL query to execute.", + }, + }, + "required": ["sql_query"], + "additionalProperties": False, + }, + }, + { + "name": "postgres_get_schema", + "description": "Retrieve the schema of the PostgreSQL database, including tables and their columns. Use this to understand the database structure before executing queries. db_name is 'default' if not provided.", + "parameters": { + "type": "object", + "properties": { + "db_name": { + "type": "string", + "description": "The name of the database to retrieve the schema for.", + }, + }, + "required": ["db_name"], + "additionalProperties": False, + }, + }, + ] + + def get_config_requirements(self): + return { + "token": { + "type": "string", + "description": "PostgreSQL database connection string (e.g., 'postgresql://user:password@host:port/dbname')", + }, + } \ No newline at end of file diff --git a/application/tools/implementations/telegram.py b/application/tools/implementations/telegram.py new file mode 100644 index 00000000..a32bbe88 --- /dev/null +++ b/application/tools/implementations/telegram.py @@ -0,0 +1,86 @@ +import requests +from application.tools.base import Tool + + +class TelegramTool(Tool): + """ + Telegram Bot + A flexible Telegram tool for performing various actions (e.g., sending messages, images). + Requires a bot token and chat ID for configuration + """ + + def __init__(self, config): + self.config = config + self.token = config.get("token", "") + + def execute_action(self, action_name, **kwargs): + actions = { + "telegram_send_message": self._send_message, + "telegram_send_image": self._send_image, + } + + if action_name in actions: + return actions[action_name](**kwargs) + else: + raise ValueError(f"Unknown action: {action_name}") + + def _send_message(self, text, chat_id): + print(f"Sending message: {text}") + url = f"https://api.telegram.org/bot{self.token}/sendMessage" + payload = {"chat_id": chat_id, "text": text} + response = requests.post(url, data=payload) + return {"status_code": response.status_code, "message": "Message sent"} + + def _send_image(self, image_url, chat_id): + print(f"Sending image: {image_url}") + url = f"https://api.telegram.org/bot{self.token}/sendPhoto" + payload = {"chat_id": chat_id, "photo": image_url} + response = requests.post(url, data=payload) + return {"status_code": response.status_code, "message": "Image sent"} + + def get_actions_metadata(self): + return [ + { + "name": "telegram_send_message", + "description": "Send a notification to Telegram chat", + "parameters": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "Text to send in the notification", + }, + "chat_id": { + "type": "string", + "description": "Chat ID to send the notification to", + }, + }, + "required": ["text"], + "additionalProperties": False, + }, + }, + { + "name": "telegram_send_image", + "description": "Send an image to the Telegram chat", + "parameters": { + "type": "object", + "properties": { + "image_url": { + "type": "string", + "description": "URL of the image to send", + }, + "chat_id": { + "type": "string", + "description": "Chat ID to send the image to", + }, + }, + "required": ["image_url"], + "additionalProperties": False, + }, + }, + ] + + def get_config_requirements(self): + return { + "token": {"type": "string", "description": "Bot token for authentication"}, + } diff --git a/application/tools/llm_handler.py b/application/tools/llm_handler.py new file mode 100644 index 00000000..334d2c4c --- /dev/null +++ b/application/tools/llm_handler.py @@ -0,0 +1,112 @@ +import json +from abc import ABC, abstractmethod + + +class LLMHandler(ABC): + @abstractmethod + def handle_response(self, agent, resp, tools_dict, messages, **kwargs): + pass + + +class OpenAILLMHandler(LLMHandler): + def handle_response(self, agent, resp, tools_dict, messages): + while resp.finish_reason == "tool_calls": + message = json.loads(resp.model_dump_json())["message"] + keys_to_remove = {"audio", "function_call", "refusal"} + filtered_data = { + k: v for k, v in message.items() if k not in keys_to_remove + } + messages.append(filtered_data) + + tool_calls = resp.message.tool_calls + for call in tool_calls: + try: + tool_response, call_id = agent._execute_tool_action( + tools_dict, call + ) + function_call_dict = { + "function_call": { + "name": call.function.name, + "args": call.function.arguments, + "call_id": call_id, + } + } + function_response_dict = { + "function_response": { + "name": call.function.name, + "response": {"result": tool_response}, + "call_id": call_id, + } + } + + messages.append( + {"role": "assistant", "content": [function_call_dict]} + ) + messages.append( + {"role": "tool", "content": [function_response_dict]} + ) + + except Exception as e: + messages.append( + { + "role": "tool", + "content": f"Error executing tool: {str(e)}", + "tool_call_id": call_id, + } + ) + resp = agent.llm.gen( + model=agent.gpt_model, messages=messages, tools=agent.tools + ) + return resp + + +class GoogleLLMHandler(LLMHandler): + def handle_response(self, agent, resp, tools_dict, messages): + from google.genai import types + + while True: + response = agent.llm.gen( + model=agent.gpt_model, messages=messages, tools=agent.tools + ) + if response.candidates and response.candidates[0].content.parts: + tool_call_found = False + for part in response.candidates[0].content.parts: + if part.function_call: + tool_call_found = True + tool_response, call_id = agent._execute_tool_action( + tools_dict, part.function_call + ) + function_response_part = types.Part.from_function_response( + name=part.function_call.name, + response={"result": tool_response}, + ) + + messages.append( + {"role": "model", "content": [part.to_json_dict()]} + ) + messages.append( + { + "role": "tool", + "content": [function_response_part.to_json_dict()], + } + ) + + if ( + not tool_call_found + and response.candidates[0].content.parts + and response.candidates[0].content.parts[0].text + ): + return response.candidates[0].content.parts[0].text + elif not tool_call_found: + return response.candidates[0].content.parts + + else: + return response + + +def get_llm_handler(llm_type): + handlers = { + "openai": OpenAILLMHandler(), + "google": GoogleLLMHandler(), + } + return handlers.get(llm_type, OpenAILLMHandler()) diff --git a/application/tools/tool_action_parser.py b/application/tools/tool_action_parser.py new file mode 100644 index 00000000..ac0a70c1 --- /dev/null +++ b/application/tools/tool_action_parser.py @@ -0,0 +1,26 @@ +import json + + +class ToolActionParser: + def __init__(self, llm_type): + self.llm_type = llm_type + self.parsers = { + "OpenAILLM": self._parse_openai_llm, + "GoogleLLM": self._parse_google_llm, + } + + def parse_args(self, call): + parser = self.parsers.get(self.llm_type, self._parse_openai_llm) + return parser(call) + + def _parse_openai_llm(self, call): + call_args = json.loads(call.function.arguments) + tool_id = call.function.name.split("_")[-1] + action_name = call.function.name.rsplit("_", 1)[0] + return tool_id, action_name, call_args + + def _parse_google_llm(self, call): + call_args = call.args + tool_id = call.name.split("_")[-1] + action_name = call.name.rsplit("_", 1)[0] + return tool_id, action_name, call_args diff --git a/application/tools/tool_manager.py b/application/tools/tool_manager.py new file mode 100644 index 00000000..3e0766cf --- /dev/null +++ b/application/tools/tool_manager.py @@ -0,0 +1,46 @@ +import importlib +import inspect +import os +import pkgutil + +from application.tools.base import Tool + + +class ToolManager: + def __init__(self, config): + self.config = config + self.tools = {} + self.load_tools() + + def load_tools(self): + tools_dir = os.path.join(os.path.dirname(__file__), "implementations") + for finder, name, ispkg in pkgutil.iter_modules([tools_dir]): + if name == "base" or name.startswith("__"): + continue + module = importlib.import_module( + f"application.tools.implementations.{name}" + ) + for member_name, obj in inspect.getmembers(module, inspect.isclass): + if issubclass(obj, Tool) and obj is not Tool: + tool_config = self.config.get(name, {}) + self.tools[name] = obj(tool_config) + + def load_tool(self, tool_name, tool_config): + self.config[tool_name] = tool_config + module = importlib.import_module( + f"application.tools.implementations.{tool_name}" + ) + for member_name, obj in inspect.getmembers(module, inspect.isclass): + if issubclass(obj, Tool) and obj is not Tool: + return obj(tool_config) + + def execute_action(self, tool_name, action_name, **kwargs): + if tool_name not in self.tools: + raise ValueError(f"Tool '{tool_name}' not loaded") + return self.tools[tool_name].execute_action(action_name, **kwargs) + + def get_all_actions_metadata(self): + metadata = [] + for tool in self.tools.values(): + metadata.extend(tool.get_actions_metadata()) + return metadata diff --git a/application/tts/elevenlabs.py b/application/tts/elevenlabs.py index e1b3db27..2e8159b8 100644 --- a/application/tts/elevenlabs.py +++ b/application/tts/elevenlabs.py @@ -1,29 +1,84 @@ -from io import BytesIO +import asyncio +import websockets +import json import base64 +from io import BytesIO from application.tts.base import BaseTTS class ElevenlabsTTS(BaseTTS): - def __init__(self): - from elevenlabs.client import ElevenLabs - - self.client = ElevenLabs( - api_key="ELEVENLABS_API_KEY", - ) - + def __init__(self): + self.api_key = 'ELEVENLABS_API_KEY'# here you should put your api key + self.model = "eleven_flash_v2_5" + self.voice = "VOICE_ID" # this is the hash code for the voice not the name! + self.write_audio = 1 def text_to_speech(self, text): - lang = "en" - audio = self.client.generate( - text=text, - model="eleven_multilingual_v2", - voice="Brian", - ) - audio_data = BytesIO() - for chunk in audio: - audio_data.write(chunk) - audio_bytes = audio_data.getvalue() + asyncio.run(self._text_to_speech_websocket(text)) - # Encode to base64 - audio_base64 = base64.b64encode(audio_bytes).decode("utf-8") - return audio_base64, lang + async def _text_to_speech_websocket(self, text): + uri = f"wss://api.elevenlabs.io/v1/text-to-speech/{self.voice}/stream-input?model_id={self.model}" + websocket = await websockets.connect(uri) + payload = { + "text": " ", + "voice_settings": { + "stability": 0.5, + "similarity_boost": 0.8, + }, + "xi_api_key": self.api_key, + } + + await websocket.send(json.dumps(payload)) + + async def listen(): + while 1: + try: + msg = await websocket.recv() + data = json.loads(msg) + + if data.get("audio"): + print("audio received") + yield base64.b64decode(data["audio"]) + elif data.get("isFinal"): + break + except websockets.exceptions.ConnectionClosed: + print("websocket closed") + break + listen_task = asyncio.create_task(self.stream(listen())) + + await websocket.send(json.dumps({"text": text})) + # this is to signal the end of the text, either use this or flush + await websocket.send(json.dumps({"text": ""})) + + await listen_task + + async def stream(self, audio_stream): + if self.write_audio: + audio_bytes = BytesIO() + async for chunk in audio_stream: + if chunk: + audio_bytes.write(chunk) + with open("output_audio.mp3", "wb") as f: + f.write(audio_bytes.getvalue()) + + else: + async for chunk in audio_stream: + pass # depends on the streamer! + + +def test_elevenlabs_websocket(): + """ + Tests the ElevenlabsTTS text_to_speech method with a sample prompt. + Prints out the base64-encoded result and writes it to 'output_audio.mp3'. + """ + # Instantiate your TTS class + tts = ElevenlabsTTS() + + # Call the method with some sample text + tts.text_to_speech("Hello from ElevenLabs WebSocket!") + + print("Saved audio to output_audio.mp3.") + + +if __name__ == "__main__": + test_elevenlabs_websocket() diff --git a/application/usage.py b/application/usage.py index e87ebe38..fe4cd50e 100644 --- a/application/usage.py +++ b/application/usage.py @@ -1,7 +1,7 @@ import sys from datetime import datetime from application.core.mongo_db import MongoDB -from application.utils import num_tokens_from_string +from application.utils import num_tokens_from_string, num_tokens_from_object_or_list mongo = MongoDB.get_client() db = mongo["docsgpt"] @@ -21,11 +21,16 @@ def update_token_usage(user_api_key, token_usage): def gen_token_usage(func): - def wrapper(self, model, messages, stream, **kwargs): + def wrapper(self, model, messages, stream, tools, **kwargs): for message in messages: - self.token_usage["prompt_tokens"] += num_tokens_from_string(message["content"]) - result = func(self, model, messages, stream, **kwargs) - self.token_usage["generated_tokens"] += num_tokens_from_string(result) + if message["content"]: + self.token_usage["prompt_tokens"] += num_tokens_from_string(message["content"]) + result = func(self, model, messages, stream, tools, **kwargs) + # check if result is a string + if isinstance(result, str): + self.token_usage["generated_tokens"] += num_tokens_from_string(result) + else: + self.token_usage["generated_tokens"] += num_tokens_from_object_or_list(result) update_token_usage(self.user_api_key, self.token_usage) return result @@ -33,11 +38,11 @@ def gen_token_usage(func): def stream_token_usage(func): - def wrapper(self, model, messages, stream, **kwargs): + def wrapper(self, model, messages, stream, tools, **kwargs): for message in messages: self.token_usage["prompt_tokens"] += num_tokens_from_string(message["content"]) batch = [] - result = func(self, model, messages, stream, **kwargs) + result = func(self, model, messages, stream, tools, **kwargs) for r in result: batch.append(r) yield r diff --git a/application/utils.py b/application/utils.py index 1fc9e329..6d47d31a 100644 --- a/application/utils.py +++ b/application/utils.py @@ -1,5 +1,7 @@ -import tiktoken import hashlib +import re + +import tiktoken from flask import jsonify, make_response @@ -15,8 +17,22 @@ def get_encoding(): def num_tokens_from_string(string: str) -> int: encoding = get_encoding() - num_tokens = len(encoding.encode(string)) - return num_tokens + if isinstance(string, str): + num_tokens = len(encoding.encode(string)) + return num_tokens + else: + return 0 + + +def num_tokens_from_object_or_list(thing): + if isinstance(thing, list): + return sum([num_tokens_from_object_or_list(x) for x in thing]) + elif isinstance(thing, dict): + return sum([num_tokens_from_object_or_list(x) for x in thing.values()]) + elif isinstance(thing, str): + return num_tokens_from_string(thing) + else: + return 0 def count_tokens_docs(docs): @@ -44,5 +60,52 @@ def check_required_fields(data, required_fields): def get_hash(data): - return hashlib.md5(data.encode()).hexdigest() + return hashlib.md5(data.encode(), usedforsecurity=False).hexdigest() + +def limit_chat_history(history, max_token_limit=None, gpt_model="docsgpt"): + """ + Limits chat history based on token count. + Returns a list of messages that fit within the token limit. + """ + from application.core.settings import settings + + max_token_limit = ( + max_token_limit + if max_token_limit + and max_token_limit + < settings.MODEL_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY) + else settings.MODEL_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY) + ) + + if not history: + return [] + + trimmed_history = [] + tokens_current_history = 0 + + for message in reversed(history): + tokens_batch = 0 + if "prompt" in message and "response" in message: + tokens_batch += num_tokens_from_string(message["prompt"]) + tokens_batch += num_tokens_from_string(message["response"]) + + if "tool_calls" in message: + for tool_call in message["tool_calls"]: + tool_call_string = f"Tool: {tool_call.get('tool_name')} | Action: {tool_call.get('action_name')} | Args: {tool_call.get('arguments')} | Response: {tool_call.get('result')}" + tokens_batch += num_tokens_from_string(tool_call_string) + + if tokens_current_history + tokens_batch < max_token_limit: + tokens_current_history += tokens_batch + trimmed_history.insert(0, message) + else: + break + + return trimmed_history + + +def validate_function_name(function_name): + """Validates if a function name matches the allowed pattern.""" + if not re.match(r"^[a-zA-Z0-9_-]+$", function_name): + return False + return True diff --git a/application/vectorstore/base.py b/application/vectorstore/base.py index 9c76b89f..a6b206c9 100644 --- a/application/vectorstore/base.py +++ b/application/vectorstore/base.py @@ -75,9 +75,9 @@ class BaseVectorStore(ABC): openai_api_key=embeddings_key ) elif embeddings_name == "huggingface_sentence-transformers/all-mpnet-base-v2": - if os.path.exists("./model/all-mpnet-base-v2"): + if os.path.exists("./models/all-mpnet-base-v2"): embedding_instance = EmbeddingsSingleton.get_instance( - embeddings_name="./model/all-mpnet-base-v2", + embeddings_name = "./models/all-mpnet-base-v2", ) else: embedding_instance = EmbeddingsSingleton.get_instance( @@ -86,4 +86,5 @@ class BaseVectorStore(ABC): else: embedding_instance = EmbeddingsSingleton.get_instance(embeddings_name) - return embedding_instance \ No newline at end of file + return embedding_instance + diff --git a/application/vectorstore/faiss.py b/application/vectorstore/faiss.py index afa55db9..87ffcccb 100644 --- a/application/vectorstore/faiss.py +++ b/application/vectorstore/faiss.py @@ -1,8 +1,12 @@ -from langchain_community.vectorstores import FAISS -from application.vectorstore.base import BaseVectorStore -from application.core.settings import settings import os +from langchain_community.vectorstores import FAISS + +from application.core.settings import settings +from application.parser.schema.base import Document +from application.vectorstore.base import BaseVectorStore + + def get_vectorstore(path: str) -> str: if path: vectorstore = os.path.join("application", "indexes", path) @@ -10,21 +14,25 @@ def get_vectorstore(path: str) -> str: vectorstore = os.path.join("application") return vectorstore + class FaissStore(BaseVectorStore): def __init__(self, source_id: str, embeddings_key: str, docs_init=None): super().__init__() + self.source_id = source_id self.path = get_vectorstore(source_id) - embeddings = self._get_embeddings(settings.EMBEDDINGS_NAME, embeddings_key) + self.embeddings = self._get_embeddings(settings.EMBEDDINGS_NAME, embeddings_key) try: if docs_init: - self.docsearch = FAISS.from_documents(docs_init, embeddings) + self.docsearch = FAISS.from_documents(docs_init, self.embeddings) else: - self.docsearch = FAISS.load_local(self.path, embeddings, allow_dangerous_deserialization=True) + self.docsearch = FAISS.load_local( + self.path, self.embeddings, allow_dangerous_deserialization=True + ) except Exception: raise - self.assert_embedding_dimensions(embeddings) + self.assert_embedding_dimensions(self.embeddings) def search(self, *args, **kwargs): return self.docsearch.similarity_search(*args, **kwargs) @@ -40,11 +48,42 @@ class FaissStore(BaseVectorStore): def assert_embedding_dimensions(self, embeddings): """Check that the word embedding dimension of the docsearch index matches the dimension of the word embeddings used.""" - if settings.EMBEDDINGS_NAME == "huggingface_sentence-transformers/all-mpnet-base-v2": - word_embedding_dimension = getattr(embeddings, 'dimension', None) + if ( + settings.EMBEDDINGS_NAME + == "huggingface_sentence-transformers/all-mpnet-base-v2" + ): + word_embedding_dimension = getattr(embeddings, "dimension", None) if word_embedding_dimension is None: - raise AttributeError("'dimension' attribute not found in embeddings instance.") - + raise AttributeError( + "'dimension' attribute not found in embeddings instance." + ) + docsearch_index_dimension = self.docsearch.index.d if word_embedding_dimension != docsearch_index_dimension: - raise ValueError(f"Embedding dimension mismatch: embeddings.dimension ({word_embedding_dimension}) != docsearch index dimension ({docsearch_index_dimension})") + raise ValueError( + f"Embedding dimension mismatch: embeddings.dimension ({word_embedding_dimension}) != docsearch index dimension ({docsearch_index_dimension})" + ) + + def get_chunks(self): + chunks = [] + if self.docsearch: + for doc_id, doc in self.docsearch.docstore._dict.items(): + chunk_data = { + "doc_id": doc_id, + "text": doc.page_content, + "metadata": doc.metadata, + } + chunks.append(chunk_data) + return chunks + + def add_chunk(self, text, metadata=None): + metadata = metadata or {} + doc = Document(text=text, extra_info=metadata).to_langchain_format() + doc_id = self.docsearch.add_documents([doc]) + self.save_local(self.path) + return doc_id + + def delete_chunk(self, chunk_id): + self.delete_index([chunk_id]) + self.save_local(self.path) + return True diff --git a/application/vectorstore/mongodb.py b/application/vectorstore/mongodb.py index c577a5d5..94b757e0 100644 --- a/application/vectorstore/mongodb.py +++ b/application/vectorstore/mongodb.py @@ -124,3 +124,53 @@ class MongoDBVectorStore(BaseVectorStore): def delete_index(self, *args, **kwargs): self._collection.delete_many({"source_id": self._source_id}) + + def get_chunks(self): + try: + chunks = [] + cursor = self._collection.find({"source_id": self._source_id}) + for doc in cursor: + doc_id = str(doc.get("_id")) + text = doc.get(self._text_key) + metadata = { + k: v + for k, v in doc.items() + if k + not in ["_id", self._text_key, self._embedding_key, "source_id"] + } + + if text: + chunks.append( + {"doc_id": doc_id, "text": text, "metadata": metadata} + ) + + return chunks + except Exception as e: + print(f"Error getting chunks: {e}") + return [] + + def add_chunk(self, text, metadata=None): + metadata = metadata or {} + embeddings = self._embedding.embed_documents([text]) + if not embeddings: + raise ValueError("Could not generate embedding for chunk") + + chunk_data = { + self._text_key: text, + self._embedding_key: embeddings[0], + "source_id": self._source_id, + **metadata, + } + result = self._collection.insert_one(chunk_data) + return str(result.inserted_id) + + def delete_chunk(self, chunk_id): + try: + from bson.objectid import ObjectId + + object_id = ObjectId(chunk_id) + result = self._collection.delete_one({"_id": object_id}) + return result.deleted_count > 0 + except Exception as e: + print(f"Error deleting chunk: {e}") + return False diff --git a/application/worker.py b/application/worker.py index 33cd90e5..df0bbe7d 100755 --- a/application/worker.py +++ b/application/worker.py @@ -12,10 +12,10 @@ from bson.objectid import ObjectId from application.core.mongo_db import MongoDB from application.core.settings import settings from application.parser.file.bulk import SimpleDirectoryReader -from application.parser.open_ai_func import call_openai_api +from application.parser.embedding_pipeline import embed_and_store_documents from application.parser.remote.remote_creator import RemoteCreator from application.parser.schema.base import Document -from application.parser.token_func import group_split +from application.parser.chunking import Chunker from application.utils import count_tokens_docs mongo = MongoDB.get_client() @@ -126,7 +126,6 @@ def ingest_worker( limit = None exclude = True sample = False - token_check = True full_path = os.path.join(directory, user, name_job) logging.info(f"Ingest file: {full_path}", extra={"user": user, "job": name_job}) @@ -153,17 +152,19 @@ def ingest_worker( exclude_hidden=exclude, file_metadata=metadata_from_filename, ).load_data() - raw_docs = group_split( - documents=raw_docs, - min_tokens=MIN_TOKENS, + + chunker = Chunker( + chunking_strategy="classic_chunk", max_tokens=MAX_TOKENS, - token_check=token_check, + min_tokens=MIN_TOKENS, + duplicate_headers=False ) + raw_docs = chunker.chunk(documents=raw_docs) docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs] id = ObjectId() - call_openai_api(docs, full_path, id, self) + embed_and_store_documents(docs, full_path, id, self) tokens = count_tokens_docs(docs) self.update_state(state="PROGRESS", meta={"current": 100}) @@ -202,52 +203,61 @@ def remote_worker( sync_frequency="never", operation_mode="upload", doc_id=None, -): - token_check = True +): full_path = os.path.join(directory, user, name_job) - if not os.path.exists(full_path): os.makedirs(full_path) + self.update_state(state="PROGRESS", meta={"current": 1}) - logging.info( - f"Remote job: {full_path}", - extra={"user": user, "job": name_job, "source_data": source_data}, - ) + try: + logging.info("Initializing remote loader with type: %s", loader) + remote_loader = RemoteCreator.create_loader(loader) + raw_docs = remote_loader.load_data(source_data) - remote_loader = RemoteCreator.create_loader(loader) - raw_docs = remote_loader.load_data(source_data) + chunker = Chunker( + chunking_strategy="classic_chunk", + max_tokens=MAX_TOKENS, + min_tokens=MIN_TOKENS, + duplicate_headers=False + ) + docs = chunker.chunk(documents=raw_docs) + docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs] + tokens = count_tokens_docs(docs) + logging.info("Total tokens calculated: %d", tokens) - docs = group_split( - documents=raw_docs, - min_tokens=MIN_TOKENS, - max_tokens=MAX_TOKENS, - token_check=token_check, - ) - tokens = count_tokens_docs(docs) - if operation_mode == "upload": - id = ObjectId() - call_openai_api(docs, full_path, id, self) - elif operation_mode == "sync": - if not doc_id or not ObjectId.is_valid(doc_id): - raise ValueError("doc_id must be provided for sync operation.") - id = ObjectId(doc_id) - call_openai_api(docs, full_path, id, self) - self.update_state(state="PROGRESS", meta={"current": 100}) + if operation_mode == "upload": + id = ObjectId() + embed_and_store_documents(docs, full_path, id, self) + elif operation_mode == "sync": + if not doc_id or not ObjectId.is_valid(doc_id): + logging.error("Invalid doc_id provided for sync operation: %s", doc_id) + raise ValueError("doc_id must be provided for sync operation.") + id = ObjectId(doc_id) + embed_and_store_documents(docs, full_path, id, self) - file_data = { - "name": name_job, - "user": user, - "tokens": tokens, - "retriever": retriever, - "id": str(id), - "type": loader, - "remote_data": source_data, - "sync_frequency": sync_frequency, - } - upload_index(full_path, file_data) + self.update_state(state="PROGRESS", meta={"current": 100}) - shutil.rmtree(full_path) + file_data = { + "name": name_job, + "user": user, + "tokens": tokens, + "retriever": retriever, + "id": str(id), + "type": loader, + "remote_data": source_data, + "sync_frequency": sync_frequency, + } + upload_index(full_path, file_data) + except Exception as e: + logging.error("Error in remote_worker task: %s", str(e), exc_info=True) + raise + + finally: + if os.path.exists(full_path): + shutil.rmtree(full_path) + + logging.info("remote_worker task completed successfully") return {"urls": source_data, "name_job": name_job, "user": user, "limited": False} def sync( diff --git a/docker-compose-azure.yaml b/deployment/docker-compose-azure.yaml similarity index 87% rename from docker-compose-azure.yaml rename to deployment/docker-compose-azure.yaml index 601831e5..9e8b6fce 100644 --- a/docker-compose-azure.yaml +++ b/deployment/docker-compose-azure.yaml @@ -1,6 +1,6 @@ services: frontend: - build: ./frontend + build: ../frontend environment: - VITE_API_HOST=http://localhost:7091 - VITE_API_STREAMING=$VITE_API_STREAMING @@ -10,7 +10,7 @@ services: - backend backend: - build: ./application + build: ../application environment: - API_KEY=$OPENAI_API_KEY - EMBEDDINGS_KEY=$OPENAI_API_KEY @@ -25,15 +25,15 @@ services: ports: - "7091:7091" volumes: - - ./application/indexes:/app/application/indexes - - ./application/inputs:/app/application/inputs - - ./application/vectors:/app/application/vectors + - ../application/indexes:/app/application/indexes + - ../application/inputs:/app/application/inputs + - ../application/vectors:/app/application/vectors depends_on: - redis - mongo worker: - build: ./application + build: ../application command: celery -A application.app.celery worker -l INFO environment: - API_KEY=$OPENAI_API_KEY diff --git a/deployment/docker-compose-dev.yaml b/deployment/docker-compose-dev.yaml new file mode 100644 index 00000000..8a3e75c4 --- /dev/null +++ b/deployment/docker-compose-dev.yaml @@ -0,0 +1,18 @@ +services: + + redis: + image: redis:6-alpine + ports: + - 6379:6379 + + mongo: + image: mongo:6 + ports: + - 27017:27017 + volumes: + - mongodb_data_container:/data/db + + + +volumes: + mongodb_data_container: \ No newline at end of file diff --git a/docker-compose-local.yaml b/deployment/docker-compose-local.yaml similarity index 88% rename from docker-compose-local.yaml rename to deployment/docker-compose-local.yaml index d9fd248b..77a82866 100644 --- a/docker-compose-local.yaml +++ b/deployment/docker-compose-local.yaml @@ -1,8 +1,8 @@ services: frontend: - build: ./frontend + build: ../frontend volumes: - - ./frontend/src:/app/src + - ../frontend/src:/app/src environment: - VITE_API_HOST=http://localhost:7091 - VITE_API_STREAMING=$VITE_API_STREAMING diff --git a/docker-compose.yaml b/deployment/docker-compose.yaml similarity index 78% rename from docker-compose.yaml rename to deployment/docker-compose.yaml index d3f3421a..15d9522f 100644 --- a/docker-compose.yaml +++ b/deployment/docker-compose.yaml @@ -1,8 +1,8 @@ services: frontend: - build: ./frontend + build: ../frontend volumes: - - ./frontend/src:/app/src + - ../frontend/src:/app/src environment: - VITE_API_HOST=http://localhost:7091 - VITE_API_STREAMING=$VITE_API_STREAMING @@ -12,7 +12,7 @@ services: - backend backend: - build: ./application + build: ../application environment: - API_KEY=$API_KEY - EMBEDDINGS_KEY=$API_KEY @@ -21,18 +21,20 @@ services: - CELERY_RESULT_BACKEND=redis://redis:6379/1 - MONGO_URI=mongodb://mongo:27017/docsgpt - CACHE_REDIS_URL=redis://redis:6379/2 + - OPENAI_BASE_URL=$OPENAI_BASE_URL + - MODEL_NAME=$MODEL_NAME ports: - "7091:7091" volumes: - - ./application/indexes:/app/application/indexes - - ./application/inputs:/app/application/inputs - - ./application/vectors:/app/application/vectors + - ../application/indexes:/app/application/indexes + - ../application/inputs:/app/application/inputs + - ../application/vectors:/app/application/vectors depends_on: - redis - mongo worker: - build: ./application + build: ../application command: celery -A application.app.celery worker -l INFO -B environment: - API_KEY=$API_KEY diff --git a/k8s/deployments/docsgpt-deploy.yaml b/deployment/k8s/deployments/docsgpt-deploy.yaml similarity index 100% rename from k8s/deployments/docsgpt-deploy.yaml rename to deployment/k8s/deployments/docsgpt-deploy.yaml diff --git a/k8s/deployments/mongo-deploy.yaml b/deployment/k8s/deployments/mongo-deploy.yaml similarity index 100% rename from k8s/deployments/mongo-deploy.yaml rename to deployment/k8s/deployments/mongo-deploy.yaml diff --git a/k8s/deployments/qdrant-deploy.yaml b/deployment/k8s/deployments/qdrant-deploy.yaml similarity index 100% rename from k8s/deployments/qdrant-deploy.yaml rename to deployment/k8s/deployments/qdrant-deploy.yaml diff --git a/k8s/deployments/redis-deploy.yaml b/deployment/k8s/deployments/redis-deploy.yaml similarity index 100% rename from k8s/deployments/redis-deploy.yaml rename to deployment/k8s/deployments/redis-deploy.yaml diff --git a/k8s/docsgpt-secrets.yaml b/deployment/k8s/docsgpt-secrets.yaml similarity index 100% rename from k8s/docsgpt-secrets.yaml rename to deployment/k8s/docsgpt-secrets.yaml diff --git a/k8s/services/docsgpt-service.yaml b/deployment/k8s/services/docsgpt-service.yaml similarity index 100% rename from k8s/services/docsgpt-service.yaml rename to deployment/k8s/services/docsgpt-service.yaml diff --git a/k8s/services/mongo-service.yaml b/deployment/k8s/services/mongo-service.yaml similarity index 100% rename from k8s/services/mongo-service.yaml rename to deployment/k8s/services/mongo-service.yaml diff --git a/k8s/services/qdrant-service.yaml b/deployment/k8s/services/qdrant-service.yaml similarity index 100% rename from k8s/services/qdrant-service.yaml rename to deployment/k8s/services/qdrant-service.yaml diff --git a/k8s/services/redis-service.yaml b/deployment/k8s/services/redis-service.yaml similarity index 100% rename from k8s/services/redis-service.yaml rename to deployment/k8s/services/redis-service.yaml diff --git a/deployment/optional/docker-compose.optional.ollama-cpu.yaml b/deployment/optional/docker-compose.optional.ollama-cpu.yaml new file mode 100644 index 00000000..d7127314 --- /dev/null +++ b/deployment/optional/docker-compose.optional.ollama-cpu.yaml @@ -0,0 +1,11 @@ +version: "3.8" +services: + ollama: + image: ollama/ollama + ports: + - "11434:11434" + volumes: + - ollama_data:/root/.ollama + +volumes: + ollama_data: \ No newline at end of file diff --git a/deployment/optional/docker-compose.optional.ollama-gpu.yaml b/deployment/optional/docker-compose.optional.ollama-gpu.yaml new file mode 100644 index 00000000..17d79100 --- /dev/null +++ b/deployment/optional/docker-compose.optional.ollama-gpu.yaml @@ -0,0 +1,16 @@ +version: "3.8" +services: + ollama: + image: ollama/ollama + ports: + - "11434:11434" + volumes: + - ollama_data:/root/.ollama + deploy: + resources: + reservations: + devices: + - capabilities: [gpu] + +volumes: + ollama_data: \ No newline at end of file diff --git a/docker-compose-mock.yaml b/docker-compose-mock.yaml deleted file mode 100644 index b4a917c9..00000000 --- a/docker-compose-mock.yaml +++ /dev/null @@ -1,20 +0,0 @@ -services: - frontend: - build: ./frontend - environment: - - VITE_API_HOST=http://localhost:7091 - - VITE_API_STREAMING=$VITE_API_STREAMING - ports: - - "5173:5173" - depends_on: - - mock-backend - - mock-backend: - build: ./mock-backend - ports: - - "7091:7091" - - redis: - image: redis:6-alpine - ports: - - 6379:6379 diff --git a/docs/components/DeploymentCards.jsx b/docs/components/DeploymentCards.jsx new file mode 100644 index 00000000..1f91c171 --- /dev/null +++ b/docs/components/DeploymentCards.jsx @@ -0,0 +1,120 @@ +import Image from 'next/image'; + +const iconMap = { + 'Amazon Lightsail': '/lightsail.png', + 'Railway': '/railway.png', + 'Civo Compute Cloud': '/civo.png', + 'DigitalOcean Droplet': '/digitalocean.png', + 'Kamatera Cloud': '/kamatera.png', +}; + + +export function DeploymentCards({ items }) { + return ( + <> +
+ {items.map(({ title, link, description }) => { + const isExternal = link.startsWith('https://'); + const iconSrc = iconMap[title] || '/default-icon.png'; // Default icon if not found + + return ( +
+ +
+ {iconSrc &&
{title}
} {/* Reduced icon size */} +
+

{title}

+ {description &&

{description}

} +

{new URL(link).hostname.replace('www.', '')}

+
+
+ ); + })} +
+ + + + ); +} \ No newline at end of file diff --git a/docs/package-lock.json b/docs/package-lock.json index 10418138..e4ffb04f 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -7,8 +7,8 @@ "license": "MIT", "dependencies": { "@vercel/analytics": "^1.1.1", - "docsgpt": "^0.4.7", - "next": "^14.2.12", + "docsgpt-react": "^0.4.9", + "next": "^14.2.22", "nextra": "^2.13.2", "nextra-theme-docs": "^2.13.2", "react": "^18.2.0", @@ -931,14 +931,14 @@ } }, "node_modules/@next/env": { - "version": "14.2.12", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.12.tgz", - "integrity": "sha512-3fP29GIetdwVIfIRyLKM7KrvJaqepv+6pVodEbx0P5CaMLYBtx+7eEg8JYO5L9sveJO87z9eCReceZLi0hxO1Q==" + "version": "14.2.22", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.22.tgz", + "integrity": "sha512-EQ6y1QeNQglNmNIXvwP/Bb+lf7n9WtgcWvtoFsHquVLCJUuxRs+6SfZ5EK0/EqkkLex4RrDySvKgKNN7PXip7Q==" }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.12", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.12.tgz", - "integrity": "sha512-crHJ9UoinXeFbHYNok6VZqjKnd8rTd7K3Z2zpyzF1ch7vVNKmhjv/V7EHxep3ILoN8JB9AdRn/EtVVyG9AkCXw==", + "version": "14.2.22", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.22.tgz", + "integrity": "sha512-HUaLiehovgnqY4TMBZJ3pDaOsTE1spIXeR10pWgdQVPYqDGQmHJBj3h3V6yC0uuo/RoY2GC0YBFRkOX3dI9WVQ==", "cpu": [ "arm64" ], @@ -951,9 +951,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.12", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.12.tgz", - "integrity": "sha512-JbEaGbWq18BuNBO+lCtKfxl563Uw9oy2TodnN2ioX00u7V1uzrsSUcg3Ep9ce+P0Z9es+JmsvL2/rLphz+Frcw==", + "version": "14.2.22", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.22.tgz", + "integrity": "sha512-ApVDANousaAGrosWvxoGdLT0uvLBUC+srqOcpXuyfglA40cP2LBFaGmBjhgpxYk5z4xmunzqQvcIgXawTzo2uQ==", "cpu": [ "x64" ], @@ -966,9 +966,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.12", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.12.tgz", - "integrity": "sha512-qBy7OiXOqZrdp88QEl2H4fWalMGnSCrr1agT/AVDndlyw2YJQA89f3ttR/AkEIP9EkBXXeGl6cC72/EZT5r6rw==", + "version": "14.2.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.22.tgz", + "integrity": "sha512-3O2J99Bk9aM+d4CGn9eEayJXHuH9QLx0BctvWyuUGtJ3/mH6lkfAPRI4FidmHMBQBB4UcvLMfNf8vF0NZT7iKw==", "cpu": [ "arm64" ], @@ -981,9 +981,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.12", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.12.tgz", - "integrity": "sha512-EfD9L7o9biaQxjwP1uWXnk3vYZi64NVcKUN83hpVkKocB7ogJfyH2r7o1pPnMtir6gHZiGCeHKagJ0yrNSLNHw==", + "version": "14.2.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.22.tgz", + "integrity": "sha512-H/hqfRz75yy60y5Eg7DxYfbmHMjv60Dsa6IWHzpJSz4MRkZNy5eDnEW9wyts9bkxwbOVZNPHeb3NkqanP+nGPg==", "cpu": [ "arm64" ], @@ -996,9 +996,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.12", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.12.tgz", - "integrity": "sha512-iQ+n2pxklJew9IpE47hE/VgjmljlHqtcD5UhZVeHICTPbLyrgPehaKf2wLRNjYH75udroBNCgrSSVSVpAbNoYw==", + "version": "14.2.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.22.tgz", + "integrity": "sha512-LckLwlCLcGR1hlI5eiJymR8zSHPsuruuwaZ3H2uudr25+Dpzo6cRFjp/3OR5UYJt8LSwlXv9mmY4oI2QynwpqQ==", "cpu": [ "x64" ], @@ -1011,9 +1011,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.12", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.12.tgz", - "integrity": "sha512-rFkUkNwcQ0ODn7cxvcVdpHlcOpYxMeyMfkJuzaT74xjAa5v4fxP4xDk5OoYmPi8QNLDs3UgZPMSBmpBuv9zKWA==", + "version": "14.2.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.22.tgz", + "integrity": "sha512-qGUutzmh0PoFU0fCSu0XYpOfT7ydBZgDfcETIeft46abPqP+dmePhwRGLhFKwZWxNWQCPprH26TjaTxM0Nv8mw==", "cpu": [ "x64" ], @@ -1026,9 +1026,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.12", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.12.tgz", - "integrity": "sha512-PQFYUvwtHs/u0K85SG4sAdDXYIPXpETf9mcEjWc0R4JmjgMKSDwIU/qfZdavtP6MPNiMjuKGXHCtyhR/M5zo8g==", + "version": "14.2.22", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.22.tgz", + "integrity": "sha512-K6MwucMWmIvMb9GlvT0haYsfIPxfQD8yXqxwFy4uLFMeXIb2TcVYQimxkaFZv86I7sn1NOZnpOaVk5eaxThGIw==", "cpu": [ "arm64" ], @@ -1041,9 +1041,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.12", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.12.tgz", - "integrity": "sha512-FAj2hMlcbeCV546eU2tEv41dcJb4NeqFlSXU/xL/0ehXywHnNpaYajOUvn3P8wru5WyQe6cTZ8fvckj/2XN4Vw==", + "version": "14.2.22", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.22.tgz", + "integrity": "sha512-5IhDDTPEbzPR31ZzqHe90LnNe7BlJUZvC4sA1thPJV6oN5WmtWjZ0bOYfNsyZx00FJt7gggNs6SrsX0UEIcIpA==", "cpu": [ "ia32" ], @@ -1056,9 +1056,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.12", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.12.tgz", - "integrity": "sha512-yu8QvV53sBzoIVRHsxCHqeuS8jYq6Lrmdh0briivuh+Brsp6xjg80MAozUsBTAV9KNmY08KlX0KYTWz1lbPzEg==", + "version": "14.2.22", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.22.tgz", + "integrity": "sha512-nvRaB1PyG4scn9/qNzlkwEwLzuoPH3Gjp7Q/pLuwUgOTt1oPMlnCI3A3rgkt+eZnU71emOiEv/mR201HoURPGg==", "cpu": [ "x64" ], @@ -1170,6 +1170,407 @@ "node": ">=8" } }, + "node_modules/@parcel/core": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.13.2.tgz", + "integrity": "sha512-1zC5Au4z9or5XyP6ipfvJqHktuB0jD7WuxMcV1CWAZGARHKylLe+0ccl+Wx7HN5O+xAvfCDtTlKrATY8qyrIyw==", + "peer": true, + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "@parcel/cache": "2.13.2", + "@parcel/diagnostic": "2.13.2", + "@parcel/events": "2.13.2", + "@parcel/feature-flags": "2.13.2", + "@parcel/fs": "2.13.2", + "@parcel/graph": "3.3.2", + "@parcel/logger": "2.13.2", + "@parcel/package-manager": "2.13.2", + "@parcel/plugin": "2.13.2", + "@parcel/profiler": "2.13.2", + "@parcel/rust": "2.13.2", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.13.2", + "@parcel/utils": "2.13.2", + "@parcel/workers": "2.13.2", + "base-x": "^3.0.8", + "browserslist": "^4.6.6", + "clone": "^2.1.1", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "json5": "^2.2.0", + "msgpackr": "^1.9.9", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/cache": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/cache/-/cache-2.13.2.tgz", + "integrity": "sha512-Y0nWlCMWDSp1lxiPI5zCWTGD0InnVZ+IfqeyLWmROAqValYyd0QZCvnSljKJ144jWTr0jXxDveir+DVF8sAYaA==", + "peer": true, + "dependencies": { + "@parcel/fs": "2.13.2", + "@parcel/logger": "2.13.2", + "@parcel/utils": "2.13.2", + "lmdb": "2.8.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.13.2" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/codeframe": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/codeframe/-/codeframe-2.13.2.tgz", + "integrity": "sha512-qFMiS14orb6QSQj5/J/QN+gJElUfedVAKBTNkp9QB4i8ObdLHDqHRUzFb55ZQJI3G4vsxOOWAOUXGirtLwrxGQ==", + "peer": true, + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/diagnostic": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.13.2.tgz", + "integrity": "sha512-6Au0JEJ5SY2gYrY0/m0i0sTuqTvK0k2E9azhBJR+zzCREbUxLiDdLZ+vXAfLW7t/kPAcWtdNU0Bj7pnZcMiMXg==", + "peer": true, + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/events": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/events/-/events-2.13.2.tgz", + "integrity": "sha512-BVB9hW1RGh/tMaDHfpa+uIgz5PMULorCnjmWr/KvrlhdUSUQoaPYfRcTDYrKhoKuNIKsWSnTGvXrxE53L5qo0w==", + "peer": true, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/fs": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.13.2.tgz", + "integrity": "sha512-bdeIMuAXhMnROvqV55JWRUmjD438/T7h3r3NsFnkq+Mp4z2nuAn0STxbqDNxIgTMJHNunSDzncqRNMT7xJCe8A==", + "peer": true, + "dependencies": { + "@parcel/feature-flags": "2.13.2", + "@parcel/rust": "2.13.2", + "@parcel/types-internal": "2.13.2", + "@parcel/utils": "2.13.2", + "@parcel/watcher": "^2.0.7", + "@parcel/workers": "2.13.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.13.2" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/logger": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.13.2.tgz", + "integrity": "sha512-SFVABAMqaT9jIDn4maPgaQQauPDz8fpoKUGEuLF44Q0aQFbBUy7vX7KYs/EvYSWZo4VyJcUDHvIInBlepA0/ZQ==", + "peer": true, + "dependencies": { + "@parcel/diagnostic": "2.13.2", + "@parcel/events": "2.13.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/markdown-ansi": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/markdown-ansi/-/markdown-ansi-2.13.2.tgz", + "integrity": "sha512-MIEoetfT/snk1GqWzBI3AhifV257i2xke9dvyQl14PPiMl+TlVhwnbQyA09WJBvDor+MuxZypHL7xoFdW8ff3A==", + "peer": true, + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/node-resolver-core": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.4.2.tgz", + "integrity": "sha512-SwnKLcZRG1VdB5JeM/Ax5VMWWh2QfXufmMQCKKx0/Kk41nUpie+aIZKj3LH6Z/fJsnKig/vXpeWoxGhmG523qg==", + "peer": true, + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "@parcel/diagnostic": "2.13.2", + "@parcel/fs": "2.13.2", + "@parcel/rust": "2.13.2", + "@parcel/utils": "2.13.2", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/package-manager": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.13.2.tgz", + "integrity": "sha512-6HjfbdJUjHyNKzYB7GSYnOCtLwqCGW7yT95GlnnTKyFffvXYsqvBSyepMuPRlbX0mFUm4S9l2DH3OVZrk108AA==", + "peer": true, + "dependencies": { + "@parcel/diagnostic": "2.13.2", + "@parcel/fs": "2.13.2", + "@parcel/logger": "2.13.2", + "@parcel/node-resolver-core": "3.4.2", + "@parcel/types": "2.13.2", + "@parcel/utils": "2.13.2", + "@parcel/workers": "2.13.2", + "@swc/core": "^1.7.26", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.13.2" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/plugin": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.13.2.tgz", + "integrity": "sha512-Q+RIENS1B185yLPhrGdzBK1oJrZmh/RXrYMnzJs78Tog8SpihjeNBNR6z4PT85o2F+Gy2y1S9A26fpiGq161qQ==", + "peer": true, + "dependencies": { + "@parcel/types": "2.13.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/profiler": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/profiler/-/profiler-2.13.2.tgz", + "integrity": "sha512-fur6Oq2HkX6AiM8rtqmDvldH5JWz0sqXA1ylz8cE3XOiDZIuvCulZmQ+hH+4odaNH6QocI1MwfV+GDh3HlQoCA==", + "peer": true, + "dependencies": { + "@parcel/diagnostic": "2.13.2", + "@parcel/events": "2.13.2", + "@parcel/types-internal": "2.13.2", + "chrome-trace-event": "^1.0.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/rust": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/rust/-/rust-2.13.2.tgz", + "integrity": "sha512-XFIewSwxkrDYOnnSP/XZ1LDLdXTs7L9CjQUWtl46Vir5Pq/rinemwLJeKGIwKLHy7fhUZQjYxquH6fBL+AY8DA==", + "peer": true, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/types": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.13.2.tgz", + "integrity": "sha512-6ixqjk2pjKELn4sQ/jdvpbCVTeH6xXQTdotkN8Wzk68F2K2MtSPIRAEocumlexScfffbRQplr2MdIf1JJWLogA==", + "peer": true, + "dependencies": { + "@parcel/types-internal": "2.13.2", + "@parcel/workers": "2.13.2" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/utils": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.13.2.tgz", + "integrity": "sha512-BkFtRo5xenmonwnBy+X4sVbHIRrx+ZHMPpS/6hFqyTvoUUFq2yTFQnfRGVVOOvscVUxpGom+kewnrTG3HHbZoA==", + "peer": true, + "dependencies": { + "@parcel/codeframe": "2.13.2", + "@parcel/diagnostic": "2.13.2", + "@parcel/logger": "2.13.2", + "@parcel/markdown-ansi": "2.13.2", + "@parcel/rust": "2.13.2", + "@parcel/source-map": "^2.1.1", + "chalk": "^4.1.2", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/@parcel/workers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-2.13.2.tgz", + "integrity": "sha512-P78BpH0yTT9KK09wgK4eabtlb5OlcWAmZebOToN5UYuwWEylKt0gWZx1+d+LPQupvK84/iZ+AutDScsATjgUMw==", + "peer": true, + "dependencies": { + "@parcel/diagnostic": "2.13.2", + "@parcel/logger": "2.13.2", + "@parcel/profiler": "2.13.2", + "@parcel/types-internal": "2.13.2", + "@parcel/utils": "2.13.2", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.13.2" + } + }, + "node_modules/@parcel/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@parcel/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@parcel/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@parcel/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "peer": true + }, + "node_modules/@parcel/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/core/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@parcel/diagnostic": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.12.0.tgz", @@ -1198,6 +1599,19 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/feature-flags": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/feature-flags/-/feature-flags-2.13.2.tgz", + "integrity": "sha512-cCwDAKD4Er24EkuQ+loVZXSURpM0gAGRsLJVoBtFiCSbB3nmIJJ6FLRwSBI/5OsOUExiUXDvSpfUCA5ldGTzbw==", + "peer": true, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/fs": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.12.0.tgz", @@ -1220,6 +1634,23 @@ "@parcel/core": "^2.12.0" } }, + "node_modules/@parcel/graph": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.3.2.tgz", + "integrity": "sha512-aAysQLRr8SOonSHWqdKHMJzfcrDFXKK8IYZEurlOzosiSgZXrAK7q8b8JcaJ4r84/jlvQYNYneNZeFQxKjHXkA==", + "peer": true, + "dependencies": { + "@parcel/feature-flags": "2.13.2", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/logger": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.12.0.tgz", @@ -1569,6 +2000,35 @@ "utility-types": "^3.10.0" } }, + "node_modules/@parcel/types-internal": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/types-internal/-/types-internal-2.13.2.tgz", + "integrity": "sha512-j0zb3WNM8O/+d8CArll7/4w4AyBED3Jbo32/unz89EPVN0VklmgBrRCAI5QXDKuJAGdAZSL5/a8bNYbwl7/Wxw==", + "peer": true, + "dependencies": { + "@parcel/diagnostic": "2.13.2", + "@parcel/feature-flags": "2.13.2", + "@parcel/source-map": "^2.1.1", + "utility-types": "^3.10.0" + } + }, + "node_modules/@parcel/types-internal/node_modules/@parcel/diagnostic": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.13.2.tgz", + "integrity": "sha512-6Au0JEJ5SY2gYrY0/m0i0sTuqTvK0k2E9azhBJR+zzCREbUxLiDdLZ+vXAfLW7t/kPAcWtdNU0Bj7pnZcMiMXg==", + "peer": true, + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/utils": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.12.0.tgz", @@ -2279,13 +2739,13 @@ } }, "node_modules/@swc/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.5.tgz", - "integrity": "sha512-4/JGkG4b1Z/QwCGgx+Ub46MlzrsZvBk5JSkxm9PcZ4bSX81c+4Y94Xm3iLp5Ka8NxzS5rD4mJSpcYuN3Tw0ceg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.1.tgz", + "integrity": "sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==", "hasInstallScript": true, "dependencies": { - "@swc/counter": "^0.1.2", - "@swc/types": "^0.1.5" + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.17" }, "engines": { "node": ">=10" @@ -2295,19 +2755,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.4.5", - "@swc/core-darwin-x64": "1.4.5", - "@swc/core-linux-arm-gnueabihf": "1.4.5", - "@swc/core-linux-arm64-gnu": "1.4.5", - "@swc/core-linux-arm64-musl": "1.4.5", - "@swc/core-linux-x64-gnu": "1.4.5", - "@swc/core-linux-x64-musl": "1.4.5", - "@swc/core-win32-arm64-msvc": "1.4.5", - "@swc/core-win32-ia32-msvc": "1.4.5", - "@swc/core-win32-x64-msvc": "1.4.5" + "@swc/core-darwin-arm64": "1.10.1", + "@swc/core-darwin-x64": "1.10.1", + "@swc/core-linux-arm-gnueabihf": "1.10.1", + "@swc/core-linux-arm64-gnu": "1.10.1", + "@swc/core-linux-arm64-musl": "1.10.1", + "@swc/core-linux-x64-gnu": "1.10.1", + "@swc/core-linux-x64-musl": "1.10.1", + "@swc/core-win32-arm64-msvc": "1.10.1", + "@swc/core-win32-ia32-msvc": "1.10.1", + "@swc/core-win32-x64-msvc": "1.10.1" }, "peerDependencies": { - "@swc/helpers": "^0.5.0" + "@swc/helpers": "*" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -2316,9 +2776,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.5.tgz", - "integrity": "sha512-toMSkbByHNfGXESyY1aiq5L3KutgijrNWB/THgdHIA1aIbwtrgMdFQfxpSE+INuuvWYi/Fxarv86EnU7ewbI0Q==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.1.tgz", + "integrity": "sha512-NyELPp8EsVZtxH/mEqvzSyWpfPJ1lugpTQcSlMduZLj1EASLO4sC8wt8hmL1aizRlsbjCX+r0PyL+l0xQ64/6Q==", "cpu": [ "arm64" ], @@ -2331,9 +2791,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.5.tgz", - "integrity": "sha512-LN8cbnmb4Gav8UcbBc+L/DEthmzCWZz22rQr6fIEHMN+f0d71fuKnV0ca0hoKbpZn33dlzUmXQE53HRjlRUQbw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.1.tgz", + "integrity": "sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==", "cpu": [ "x64" ], @@ -2346,9 +2806,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.5.tgz", - "integrity": "sha512-suRFkhBWmOQxlM4frpos1uqjmHfaEI8FuJ0LL5+yRE7IunNDeQJBKujGZt6taeuxo1KqC0N0Ajr8IluN2wrKpA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.1.tgz", + "integrity": "sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==", "cpu": [ "arm" ], @@ -2361,9 +2821,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.5.tgz", - "integrity": "sha512-mLKxasQArDGmR6k9c0tkPVUdoo8VfUecocMG1Mx9NYvpidJNaZ3xq9nYM77v7uq1fQqrs/59DM1fJTNRWvv/UQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.1.tgz", + "integrity": "sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==", "cpu": [ "arm64" ], @@ -2376,9 +2836,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.5.tgz", - "integrity": "sha512-pgKuyRP7S29U/HMDTx+x8dFcklWxwB9cHFNCNWSE6bS4vHR93jc4quwPX9OEQX5CVHxm+c8+xof043I4OGkAXw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.1.tgz", + "integrity": "sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==", "cpu": [ "arm64" ], @@ -2391,9 +2851,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.5.tgz", - "integrity": "sha512-srR+YN86Oerzoghd0DPCzTbTp08feeJPSr9kkNdmtQWENOa4l/9cJV3+XY6vviw0sEjezPmYnc3SwRxJRaxvEw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.1.tgz", + "integrity": "sha512-yyYEwQcObV3AUsC79rSzN9z6kiWxKAVJ6Ntwq2N9YoZqSPYph+4/Am5fM1xEQYf/kb99csj0FgOelomJSobxQA==", "cpu": [ "x64" ], @@ -2406,9 +2866,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.5.tgz", - "integrity": "sha512-aSf41LZtDeG5VXI4RCnzcu0UInPyNm3ip8Kw+sCK+sSqW9o7DgBkyqqbip3RZq84fNUHBQQQQdKXetltsyRRqw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.1.tgz", + "integrity": "sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==", "cpu": [ "x64" ], @@ -2421,9 +2881,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.5.tgz", - "integrity": "sha512-vU3k8JwRUlTkJMfJQY9E4VvLrsIFOpfhnvbuXB84Amo1cJsz+bYQcC6RSvY7qpaDzDKFdUGbJco4uZTRoRf7Mg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.1.tgz", + "integrity": "sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==", "cpu": [ "arm64" ], @@ -2436,9 +2896,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.5.tgz", - "integrity": "sha512-856YRh3frRK2XbrSjDOFBgoAqWJLNRkaEtfGzXfeEoyJlOz0BFsSJHxKlHAFkxRfHe2li9DJRUQFTEhXn4OUWw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.1.tgz", + "integrity": "sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==", "cpu": [ "ia32" ], @@ -2451,9 +2911,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.5.tgz", - "integrity": "sha512-j1+kV7jmWY1+NbXAvxAEW165781yLXVZKLcoXIZKmw18EatqMF6w8acg1gDG8C+Iw5aWLkRZVS4pijSh7+DtCQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.1.tgz", + "integrity": "sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==", "cpu": [ "x64" ], @@ -2480,9 +2940,12 @@ } }, "node_modules/@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==" + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", + "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", + "dependencies": { + "@swc/counter": "^0.1.3" + } }, "node_modules/@theguild/remark-mermaid": { "version": "0.0.5", @@ -2723,6 +3186,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/base-x": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -2937,6 +3409,15 @@ "node": ">=4" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "peer": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", @@ -3575,11 +4056,10 @@ "node": ">=0.3.1" } }, - "node_modules/docsgpt": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/docsgpt/-/docsgpt-0.4.7.tgz", - "integrity": "sha512-4YZzLZo6ybudFrJVUQflDFeWzFiTATRWB9myrGSpLigyuMMzax1ZAY2xFallZLuEG9VVm0mOgkx3ssWHLrXWkQ==", - "license": "Apache-2.0", + "node_modules/docsgpt-react": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/docsgpt-react/-/docsgpt-react-0.4.9.tgz", + "integrity": "sha512-mGGbd4IGVHrQVVdgoej991Vpl/hYkTuKz5Ax95hvqSbWDZELZnEx2/AZajAII5AayUZKWYaEFRluewUiGJVSbA==", "dependencies": { "@babel/plugin-transform-flow-strip-types": "^7.23.3", "@parcel/resolver-glob": "^2.12.0", @@ -3665,6 +4145,33 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "peer": true, + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.693", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.693.tgz", @@ -4678,9 +5185,9 @@ "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "node_modules/katex": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz", - "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==", + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -6234,9 +6741,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -6251,11 +6758,11 @@ } }, "node_modules/next": { - "version": "14.2.12", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.12.tgz", - "integrity": "sha512-cDOtUSIeoOvt1skKNihdExWMTybx3exnvbFbb9ecZDIxlvIbREQzt9A5Km3Zn3PfU+IFjyYGsHS+lN9VInAGKA==", + "version": "14.2.22", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.22.tgz", + "integrity": "sha512-Ps2caobQ9hlEhscLPiPm3J3SYhfwfpMqzsoCMZGWxt9jBRK9hoBZj2A37i8joKhsyth2EuVKDVJCTF5/H4iEDw==", "dependencies": { - "@next/env": "14.2.12", + "@next/env": "14.2.22", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -6270,15 +6777,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.12", - "@next/swc-darwin-x64": "14.2.12", - "@next/swc-linux-arm64-gnu": "14.2.12", - "@next/swc-linux-arm64-musl": "14.2.12", - "@next/swc-linux-x64-gnu": "14.2.12", - "@next/swc-linux-x64-musl": "14.2.12", - "@next/swc-win32-arm64-msvc": "14.2.12", - "@next/swc-win32-ia32-msvc": "14.2.12", - "@next/swc-win32-x64-msvc": "14.2.12" + "@next/swc-darwin-arm64": "14.2.22", + "@next/swc-darwin-x64": "14.2.22", + "@next/swc-linux-arm64-gnu": "14.2.22", + "@next/swc-linux-arm64-musl": "14.2.22", + "@next/swc-linux-x64-gnu": "14.2.22", + "@next/swc-linux-x64-musl": "14.2.22", + "@next/swc-win32-arm64-msvc": "14.2.22", + "@next/swc-win32-ia32-msvc": "14.2.22", + "@next/swc-win32-x64-msvc": "14.2.22" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -9598,6 +10105,26 @@ "node": ">=6" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peer": true + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9942,6 +10469,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", diff --git a/docs/package.json b/docs/package.json index cc3e786d..8e6ad1d3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,8 +7,8 @@ "license": "MIT", "dependencies": { "@vercel/analytics": "^1.1.1", - "docsgpt": "^0.4.7", - "next": "^14.2.12", + "docsgpt-react": "^0.4.9", + "next": "^14.2.22", "nextra": "^2.13.2", "nextra-theme-docs": "^2.13.2", "react": "^18.2.0", diff --git a/docs/pages/API/API-docs.md b/docs/pages/API/API-docs.md deleted file mode 100644 index a85ed6f8..00000000 --- a/docs/pages/API/API-docs.md +++ /dev/null @@ -1,350 +0,0 @@ -# API Endpoints Documentation - -*Currently, the application provides the following main API endpoints:* - - -### 1. /api/answer -**Description:** - -This endpoint is used to request answers to user-provided questions. - -**Request:** - -**Method**: `POST` - -**Headers**: Content-Type should be set to `application/json; charset=utf-8` - -**Request Body**: JSON object with the following fields: -* `question` — The user's question. -* `history` — (Optional) Previous conversation history. -* `api_key`— Your API key. -* `embeddings_key` — Your embeddings key. -* `active_docs` — The location of active documentation. - -Here is a JavaScript Fetch Request example: -```js -// answer (POST http://127.0.0.1:5000/api/answer) -fetch("http://127.0.0.1:5000/api/answer", { - "method": "POST", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": JSON.stringify({"question":"Hi","history":null,"api_key":"OPENAI_API_KEY","embeddings_key":"OPENAI_API_KEY", - "active_docs": "javascript/.project/ES2015/openai_text-embedding-ada-002/"}) -}) -.then((res) => res.text()) -.then(console.log.bind(console)) -``` - -**Response** - -In response, you will get a JSON document containing the `answer`, `query` and `result`: -```json -{ - "answer": "Hi there! How can I help you?\n", - "query": "Hi", - "result": "Hi there! How can I help you?\nSOURCES:" -} -``` - -### 2. /api/docs_check - -**Description:** - -This endpoint will make sure documentation is loaded on the server (just run it every time user is switching between libraries (documentations)). - -**Request:** - -**Method**: `POST` - -**Headers**: Content-Type should be set to `application/json; charset=utf-8` - -**Request Body**: JSON object with the field: -* `docs` — The location of the documentation: -```js -// docs_check (POST http://127.0.0.1:5000/api/docs_check) -fetch("http://127.0.0.1:5000/api/docs_check", { - "method": "POST", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": JSON.stringify({"docs":"javascript/.project/ES2015/openai_text-embedding-ada-002/"}) -}) -.then((res) => res.text()) -.then(console.log.bind(console)) -``` - -**Response:** - -In response, you will get a JSON document like this one indicating whether the documentation exists or not: -```json -{ - "status": "exists" -} -``` - - -### 3. /api/combine -**Description:** - -This endpoint provides information about available vectors and their locations with a simple GET request. - -**Request:** - -**Method**: `GET` - -**Response:** - -Response will include: -* `date` -* `description` -* `docLink` -* `fullName` -* `language` -* `location` (local or docshub) -* `model` -* `name` -* `version` - -Example of JSON in Docshub and local: - -image - -### 4. /api/upload -**Description:** - -This endpoint is used to upload a file that needs to be trained, response is JSON with task ID, which can be used to check on task's progress. - -**Request:** - -**Method**: `POST` - -**Request Body**: A multipart/form-data form with file upload and additional fields, including `user` and `name`. - -HTML example: - -```html -
- - - - - -
-``` - -**Response:** - -JSON response with a status and a task ID that can be used to check the task's progress. - - -### 5. /api/task_status -**Description:** - -This endpoint is used to get the status of a task (`task_id`) from `/api/upload` - -**Request:** - -**Method**: `GET` - -**Query Parameter**: `task_id` (task ID to check) - -**Sample JavaScript Fetch Request:** -```js -// Task status (Get http://127.0.0.1:5000/api/task_status) -fetch("http://localhost:5001/api/task_status?task_id=YOUR_TASK_ID", { - "method": "GET", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, -}) -.then((res) => res.text()) -.then(console.log.bind(console)) -``` - -**Response:** - -There are two types of responses: - -1. While the task is still running, the 'current' value will show progress from 0 to 100. - ```json - { - "result": { - "current": 1 - }, - "status": "PROGRESS" - } - ``` - -2. When task is completed: - ```json - { - "result": { - "directory": "temp", - "filename": "install.rst", - "formats": [ - ".rst", - ".md", - ".pdf" - ], - "name_job": "somename", - "user": "local" - }, - "status": "SUCCESS" - } - ``` - -### 6. /api/delete_old -**Description:** - -This endpoint is used to delete old Vector Stores. - -**Request:** - -**Method**: `GET` - -**Query Parameter**: `task_id` - -**Sample JavaScript Fetch Request:** -```js -// delete_old (GET http://127.0.0.1:5000/api/delete_old) -fetch("http://localhost:5001/api/delete_old?task_id=YOUR_TASK_ID", { - "method": "GET", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, -}) -.then((res) => res.text()) -.then(console.log.bind(console)) - -``` -**Response:** - -JSON response indicating the status of the operation: - -```json -{ "status": "ok" } -``` - -### 7. /api/get_api_keys -**Description:** - -The endpoint retrieves a list of API keys for the user. - -**Request:** - -**Method**: `GET` - -**Sample JavaScript Fetch Request:** -```js -// get_api_keys (GET http://127.0.0.1:5000/api/get_api_keys) -fetch("http://localhost:5001/api/get_api_keys", { - "method": "GET", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, -}) -.then((res) => res.text()) -.then(console.log.bind(console)) - -``` -**Response:** - -JSON response with a list of created API keys: - -```json -[ - { - "id": "string", - "name": "string", - "key": "string", - "source": "string" - }, - ... - ] -``` - -### 8. /api/create_api_key - -**Description:** - -Create a new API key for the user. - -**Request:** - -**Method**: `POST` - -**Headers**: Content-Type should be set to `application/json; charset=utf-8` - -**Request Body**: JSON object with the following fields: -* `name` — A name for the API key. -* `source` — The source documents that will be used. -* `prompt_id` — The prompt ID. -* `chunks` — The number of chunks used to process an answer. - -Here is a JavaScript Fetch Request example: -```js -// create_api_key (POST http://127.0.0.1:5000/api/create_api_key) -fetch("http://127.0.0.1:5000/api/create_api_key", { - "method": "POST", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": JSON.stringify({"name":"Example Key Name", - "source":"Example Source", - "prompt_id":"creative", - "chunks":"2"}) -}) -.then((res) => res.json()) -.then(console.log.bind(console)) -``` - -**Response** - -In response, you will get a JSON document containing the `id` and `key`: -```json -{ - "id": "string", - "key": "string" -} -``` - -### 9. /api/delete_api_key - -**Description:** - -Delete an API key for the user. - -**Request:** - -**Method**: `POST` - -**Headers**: Content-Type should be set to `application/json; charset=utf-8` - -**Request Body**: JSON object with the field: -* `id` — The unique identifier of the API key to be deleted. - -Here is a JavaScript Fetch Request example: -```js -// delete_api_key (POST http://127.0.0.1:5000/api/delete_api_key) -fetch("http://127.0.0.1:5000/api/delete_api_key", { - "method": "POST", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": JSON.stringify({"id":"API_KEY_ID"}) -}) -.then((res) => res.json()) -.then(console.log.bind(console)) -``` - -**Response:** - -In response, you will get a JSON document indicating the status of the operation: -```json -{ - "status": "ok" -} -``` \ No newline at end of file diff --git a/docs/pages/API/_meta.json b/docs/pages/API/_meta.json deleted file mode 100644 index 4873d38c..00000000 --- a/docs/pages/API/_meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "API-docs": { - "title": "🗂️️ API-docs", - "href": "/API/API-docs" - }, - "api-key-guide": { - "title": "🔐 API Keys guide", - "href": "/API/api-key-guide" - } -} \ No newline at end of file diff --git a/docs/pages/Deploying/Hosting-the-app.md b/docs/pages/Deploying/Amazon-Lightsail.mdx similarity index 88% rename from docs/pages/Deploying/Hosting-the-app.md rename to docs/pages/Deploying/Amazon-Lightsail.mdx index 7dd66059..24aef74b 100644 --- a/docs/pages/Deploying/Hosting-the-app.md +++ b/docs/pages/Deploying/Amazon-Lightsail.mdx @@ -1,3 +1,9 @@ +--- +title: Hosting DocsGPT on Amazon Lightsail +description: +display: hidden +--- + # Self-hosting DocsGPT on Amazon Lightsail Here's a step-by-step guide on how to set up an Amazon Lightsail instance to host DocsGPT. @@ -73,7 +79,7 @@ To save the file, press CTRL+X, then Y, and then ENTER. Next, set the correct IP for the Backend by opening the docker-compose.yml file: -`nano docker-compose.yml` +`nano deployment/docker-compose.yaml` And Change line 7 to: `VITE_API_HOST=http://localhost:7091` to this `VITE_API_HOST=http://:7091` @@ -84,7 +90,7 @@ This will allow the frontend to connect to the backend. You're almost there! Now that all the necessary bits and pieces have been installed, it is time to run the application. To do so, use the following command: -`sudo docker-compose up -d` +`sudo docker compose -f deployment/docker-compose.yaml up -d` Launching it for the first time will take a few minutes to download all the necessary dependencies and build. @@ -101,10 +107,4 @@ Repeat the process for port `7091`. #### Access your instance -Your instance is now available at your Public IP Address on port 5173. Enjoy using DocsGPT! - -## Other Deployment Options - -- [Deploy DocsGPT on Civo Compute Cloud](https://dev.to/rutamhere/deploying-docsgpt-on-civo-compute-c) -- [Deploy DocsGPT on DigitalOcean Droplet](https://dev.to/rutamhere/deploying-docsgpt-on-digitalocean-droplet-50ea) -- [Deploy DocsGPT on Kamatera Performance Cloud](https://dev.to/rutamhere/deploying-docsgpt-on-kamatera-performance-cloud-1bj) +Your instance is now available at your Public IP Address on port 5173. Enjoy using DocsGPT! \ No newline at end of file diff --git a/docs/pages/Deploying/Development-Environment.mdx b/docs/pages/Deploying/Development-Environment.mdx new file mode 100644 index 00000000..2852be19 --- /dev/null +++ b/docs/pages/Deploying/Development-Environment.mdx @@ -0,0 +1,163 @@ +--- +title: Setting Up a Development Environment +description: Guide to setting up a development environment for DocsGPT, including backend and frontend setup. +--- + +# Setting Up a Development Environment + +This guide will walk you through setting up a development environment for DocsGPT. This setup allows you to modify and test the application's backend and frontend components. + +## 1. Spin Up MongoDB and Redis + +For development purposes, you can quickly start MongoDB and Redis containers, which are the primary database and caching systems used by DocsGPT. We provide a dedicated Docker Compose file, `docker-compose-dev.yaml`, located in the `deployment` directory, that includes only these essential services. + +You can find the `docker-compose-dev.yaml` file [here](https://github.com/arc53/DocsGPT/blob/main/deployment/docker-compose-dev.yaml). + +**Steps to start MongoDB and Redis:** + +1. Navigate to the root directory of your DocsGPT repository in your terminal. + +2. Run the following commands to build and start the containers defined in `docker-compose-dev.yaml`: + + ```bash + docker compose -f deployment/docker-compose-dev.yaml build + docker compose -f deployment/docker-compose-dev.yaml up -d + ``` + + These commands will start MongoDB and Redis in detached mode, running in the background. + +## 2. Run the Backend + +To run the DocsGPT backend locally, you'll need to set up a Python environment and install the necessary dependencies. + +**Prerequisites:** + +* **Python 3.12:** Ensure you have Python 3.12 installed on your system. You can check your Python version by running `python --version` or `python3 --version` in your terminal. + +**Steps to run the backend:** + +1. **Configure Environment Variables:** + + DocsGPT backend settings are configured using environment variables. You can set these either in a `.env` file or directly in the `settings.py` file. For a comprehensive overview of all settings, please refer to the [DocsGPT Settings Guide](/Deploying/DocsGPT-Settings). + + * **Option 1: Using a `.env` file (Recommended):** + * If you haven't already, create a file named `.env` in the **root directory** of your DocsGPT project. + * Modify the `.env` file to adjust settings as needed. You can find a comprehensive list of configurable options in [`application/core/settings.py`](application/core/settings.py). + + * **Option 2: Exporting Environment Variables:** + * Alternatively, you can export environment variables directly in your terminal. However, using a `.env` file is generally more organized for development. + +2. **Create a Python Virtual Environment (Optional but Recommended):** + + Using a virtual environment isolates project dependencies and avoids conflicts with system-wide Python packages. + + * **macOS and Linux:** + + ```bash + python -m venv venv + . venv/bin/activate + ``` + + * **Windows:** + + ```bash + python -m venv venv + venv/Scripts/activate + ``` + +3. **Download Embedding Model:** + + The backend requires an embedding model. Download the `mpnet-base-v2` model and place it in the `model/` directory within the project root. You can use the following script: + + ```bash + wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip + unzip mpnet-base-v2.zip -d model + rm mpnet-base-v2.zip + ``` + + Alternatively, you can manually download the zip file from [here](https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip), unzip it, and place the extracted folder in `model/`. + +4. **Install Backend Dependencies:** + + Navigate to the root of your DocsGPT repository and install the required Python packages: + + ```bash + pip install -r application/requirements.txt + ``` + +5. **Run the Flask App:** + + Start the Flask backend application: + + ```bash + flask --app application/app.py run --host=0.0.0.0 --port=7091 + ``` + + This command will launch the backend server, making it accessible on `http://localhost:7091`. + +6. **Start the Celery Worker:** + + Open a new terminal window (and activate your virtual environment if you used one). Start the Celery worker to handle background tasks: + + ```bash + celery -A application.app.celery worker -l INFO + ``` + + This command will start the Celery worker, which processes tasks such as document parsing and vector embedding. + +**Running in Debugger (VSCode):** + +For easier debugging, you can launch the Flask app and Celery worker directly from VSCode's debugger. + +* Press Shift + Cmd + D (macOS) or Shift + Windows + D (Windows) to open the Run and Debug view. +* You should see configurations named "Flask" and "Celery". Select the desired configuration and click the "Start Debugging" button (green play icon). + +## 3. Start the Frontend + +To run the DocsGPT frontend locally, you'll need Node.js and npm (Node Package Manager). + +**Prerequisites:** + +* **Node.js version 16 or higher:** Ensure you have Node.js version 16 or greater installed. You can check your Node.js version by running `node -v` in your terminal. npm is usually bundled with Node.js. + +**Steps to start the frontend:** + +1. **Navigate to the Frontend Directory:** + + In your terminal, change the current directory to the `frontend` folder within your DocsGPT repository: + + ```bash + cd frontend + ``` + +2. **Install Global Packages (If Needed):** + + If you don't have `husky` and `vite` installed globally, you can install them: + + ```bash + npm install husky -g + npm install vite -g + ``` + You can skip this step if you already have these packages installed or prefer to use local installations (though global installation simplifies running the commands in this guide). + +3. **Install Frontend Dependencies:** + + Install the project's frontend dependencies using npm: + + ```bash + npm install --include=dev + ``` + + This command reads the `package.json` file in the `frontend` directory and installs all listed dependencies, including development dependencies. + +4. **Run the Frontend App:** + + Start the frontend development server: + + ```bash + npm run dev + ``` + + This command will start the Vite development server. The frontend application will typically be accessible at [http://localhost:5173/](http://localhost:5173/). The terminal will display the exact URL where the frontend is running. + +With both the backend and frontend running, you should now have a fully functional DocsGPT development environment. You can access the application in your browser at [http://localhost:5173/](http://localhost:5173/) and start developing! \ No newline at end of file diff --git a/docs/pages/Deploying/Docker-Deploying.mdx b/docs/pages/Deploying/Docker-Deploying.mdx new file mode 100644 index 00000000..559fa4e3 --- /dev/null +++ b/docs/pages/Deploying/Docker-Deploying.mdx @@ -0,0 +1,135 @@ +--- +title: Docker Deployment of DocsGPT +description: Deploy DocsGPT using Docker and Docker Compose for easy setup and management. +--- + +# Docker Deployment of DocsGPT + +Docker is the recommended method for deploying DocsGPT, providing a consistent and isolated environment for the application to run. This guide will walk you through deploying DocsGPT using Docker and Docker Compose. + +## Prerequisites + +* **Docker Engine:** You need to have Docker Engine installed on your system. + * **macOS:** [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) + * **Linux:** [Docker Engine Installation Guide](https://docs.docker.com/engine/install/) (follow instructions for your specific distribution) + * **Windows:** [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) (requires WSL 2 backend, see notes below) +* **Docker Compose:** Docker Compose is usually included with Docker Desktop. If you are using Docker Engine separately, ensure you have Docker Compose V2 installed. + +**Important Note for Windows Users:** Docker Desktop on Windows generally requires the WSL 2 backend to function correctly, especially when using features like host networking which are utilized in DocsGPT's Docker Compose setup. Ensure WSL 2 is enabled and configured in Docker Desktop settings. + +## Quickest Setup: Using DocsGPT Public API + +The fastest way to try out DocsGPT is by using the public API endpoint. This requires minimal configuration and no local LLM setup. + +1. **Clone the DocsGPT Repository (if you haven't already):** + + ```bash + git clone https://github.com/arc53/DocsGPT.git + cd DocsGPT + ``` + +2. **Create a `.env` file:** + + In the root directory of your DocsGPT repository, create a file named `.env`. + +3. **Add Public API Configuration to `.env`:** + + Open the `.env` file and add the following lines: + + ``` + LLM_NAME=docsgpt + VITE_API_STREAMING=true + ``` + + This minimal configuration tells DocsGPT to use the public API. For more advanced settings and other LLM options, refer to the [DocsGPT Settings Guide](/Deploying/DocsGPT-Settings). + +4. **Launch DocsGPT with Docker Compose:** + + Navigate to the root directory of the DocsGPT repository in your terminal and run: + + ```bash + docker compose -f deployment/docker-compose.yaml up -d + ``` + + The `-d` flag runs Docker Compose in detached mode (in the background). + +5. **Access DocsGPT in your browser:** + + Once the containers are running, open your web browser and go to [http://localhost:5173/](http://localhost:5173/). + +6. **Stopping DocsGPT:** + + To stop the application, navigate to the same directory in your terminal and run: + + ```bash + docker compose -f deployment/docker-compose.yaml down + ``` + +## Optional Ollama Setup (Local Models) + +DocsGPT provides optional Docker Compose files to easily integrate with [Ollama](https://ollama.com/) for running local models. These files add an official Ollama container to your Docker Compose setup. These files are located in the `deployment/optional/` directory. + +There are two Ollama optional files: + +* **`docker-compose.optional.ollama-cpu.yaml`**: For running Ollama on CPU. +* **`docker-compose.optional.ollama-gpu.yaml`**: For running Ollama on GPU (requires Docker to be configured for GPU usage). + +### Launching with Ollama and Pulling a Model + +1. **Clone the DocsGPT Repository and Create `.env` (as described above).** + +2. **Launch DocsGPT with Ollama Docker Compose:** + + Choose the appropriate Ollama Compose file (CPU or GPU) and launch DocsGPT: + + **CPU:** + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml up -d + ``` + **GPU:** + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml up -d + ``` + +3. **Pull the Ollama Model:** + + **Crucially, after launching with Ollama, you need to pull the desired model into the Ollama container.** Find the `MODEL_NAME` you configured in your `.env` file (e.g., `llama3.2:1b`). Then execute the following command to pull the model *inside* the running Ollama container: + + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml exec -it ollama ollama pull + ``` + or (for GPU): + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml exec -it ollama ollama pull + ``` + Replace `` with the actual model name from your `.env` file. + +4. **Access DocsGPT in your browser:** + + Once the model is pulled and containers are running, open your web browser and go to [http://localhost:5173/](http://localhost:5173/). + +5. **Stopping Ollama Setup:** + + To stop a DocsGPT setup launched with Ollama optional files, use `docker compose down` and include all the compose files used during the `up` command: + + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml down + ``` + or + + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml down + ``` + +**Important for GPU Usage:** + +* **NVIDIA Container Toolkit (for NVIDIA GPUs):** If you are using NVIDIA GPUs, you need to have the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) installed and configured on your system for Docker to access your GPU. +* **Docker GPU Configuration:** Ensure Docker is configured to utilize your GPU. Refer to the [Ollama Docker Hub page](https://hub.docker.com/r/ollama/ollama) and Docker documentation for GPU setup instructions specific to your GPU type (NVIDIA, AMD, Intel). + +## Restarting After Configuration Changes + +Whenever you modify the `.env` file or any Docker Compose files, you need to restart the Docker containers for the changes to be applied. Use the same `docker compose down` and `docker compose up -d` commands you used to launch DocsGPT, ensuring you include all relevant `-f` flags for optional files if you are using them. + +## Further Configuration + +This guide covers the basic Docker deployment of DocsGPT. For detailed information on configuring various aspects of DocsGPT, such as LLM providers, models, vector stores, and more, please refer to the comprehensive [DocsGPT Settings Guide](/Deploying/DocsGPT-Settings). \ No newline at end of file diff --git a/docs/pages/Deploying/DocsGPT-Settings.mdx b/docs/pages/Deploying/DocsGPT-Settings.mdx new file mode 100644 index 00000000..ce1e46ba --- /dev/null +++ b/docs/pages/Deploying/DocsGPT-Settings.mdx @@ -0,0 +1,107 @@ +--- +title: DocsGPT Settings +description: Configure your DocsGPT application by understanding the basic settings. +--- + +# DocsGPT Settings + +DocsGPT is highly configurable, allowing you to tailor it to your specific needs and preferences. You can control various aspects of the application, from choosing the Large Language Model (LLM) provider to selecting embedding models and vector stores. + +This document will guide you through the basic settings you can configure in DocsGPT. These settings determine how DocsGPT interacts with LLMs and processes your data. + +## Configuration Methods + +There are two primary ways to configure DocsGPT settings: + +### 1. Configuration via `.env` file (Recommended) + +The easiest and recommended way to configure basic settings is by using a `.env` file. This file should be located in the **root directory** of your DocsGPT project (the same directory where `setup.sh` is located). + +**Example `.env` file structure:** + +``` +LLM_NAME=openai +API_KEY=YOUR_OPENAI_API_KEY +MODEL_NAME=gpt-4o +``` + +### 2. Configuration via `settings.py` file (Advanced) + +For more advanced configurations or if you prefer to manage settings directly in code, you can modify the `settings.py` file. This file is located in the `application/core` directory of your DocsGPT project. + +While modifying `settings.py` offers more flexibility, it's generally recommended to use the `.env` file for basic settings and reserve `settings.py` for more complex adjustments or when you need to configure settings programmatically. + +**Location of `settings.py`:** `application/core/settings.py` + +## Basic Settings Explained + +Here are some of the most fundamental settings you'll likely want to configure: + +- **`LLM_NAME`**: This setting determines which Large Language Model (LLM) provider DocsGPT will use. It tells DocsGPT which API to interact with. + + - **Common values:** + - `docsgpt`: Use the DocsGPT Public API Endpoint (simple and free, as offered in `setup.sh` option 1). + - `openai`: Use OpenAI's API (requires an API key). + - `google`: Use Google's Vertex AI or Gemini models. + - `anthropic`: Use Anthropic's Claude models. + - `groq`: Use Groq's models. + - `huggingface`: Use HuggingFace Inference API. + - `azure_openai`: Use Azure OpenAI Service. + - `openai` (when using local inference engines like Ollama, Llama.cpp, TGI, etc.): This signals DocsGPT to use an OpenAI-compatible API format, even if the actual LLM is running locally. + +- **`MODEL_NAME`**: Specifies the specific model to use from the chosen LLM provider. The available models depend on the `LLM_NAME` you've selected. + + - **Examples:** + - For `LLM_NAME=openai`: `gpt-4o` + - For `LLM_NAME=google`: `gemini-2.0-flash` + - For local models (e.g., Ollama): `llama3.2:1b` (or any model name available in your setup). + +- **`EMBEDDINGS_NAME`**: This setting defines which embedding model DocsGPT will use to generate vector embeddings for your documents. Embeddings are numerical representations of text that allow DocsGPT to understand the semantic meaning of your documents for efficient search and retrieval. + + - **Default value:** `huggingface_sentence-transformers/all-mpnet-base-v2` (a good general-purpose embedding model). + - **Other options:** You can explore other embedding models from Hugging Face Sentence Transformers or other providers if needed. + +- **`API_KEY`**: Required for most cloud-based LLM providers. This is your authentication key to access the LLM provider's API. You'll need to obtain this key from your chosen provider's platform. + +- **`OPENAI_BASE_URL`**: Specifically used when `LLM_NAME` is set to `openai` but you are connecting to a local inference engine (like Ollama, Llama.cpp, etc.) that exposes an OpenAI-compatible API. This setting tells DocsGPT where to find your local LLM server. + +## Configuration Examples + +Let's look at some concrete examples of how to configure these settings in your `.env` file. + +### Example for Cloud API Provider (OpenAI) + +To use OpenAI's `gpt-4o` model, you would configure your `.env` file like this: + +``` +LLM_NAME=openai +API_KEY=YOUR_OPENAI_API_KEY # Replace with your actual OpenAI API key +MODEL_NAME=gpt-4o +``` + +Make sure to replace `YOUR_OPENAI_API_KEY` with your actual OpenAI API key. + +### Example for Local Deployment + +To use a local Ollama server with the `llama3.2:1b` model, you would configure your `.env` file like this: + +``` +LLM_NAME=openai # Using OpenAI compatible API format for local models +API_KEY=None # API Key is not needed for local Ollama +MODEL_NAME=llama3.2:1b +OPENAI_BASE_URL=http://host.docker.internal:11434/v1 # Default Ollama API URL within Docker +EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2 # You can also run embeddings locally if needed +``` + +In this case, even though you are using Ollama locally, `LLM_NAME` is set to `openai` because Ollama (and many other local inference engines) are designed to be API-compatible with OpenAI. `OPENAI_BASE_URL` points DocsGPT to the local Ollama server. + +## Exploring More Settings + +These are just the basic settings to get you started. The `settings.py` file contains many more advanced options that you can explore to further customize DocsGPT, such as: + +- Vector store configuration (`VECTOR_STORE`, Qdrant, Milvus, LanceDB settings) +- Retriever settings (`RETRIEVERS_ENABLED`) +- Cache settings (`CACHE_REDIS_URL`) +- And many more! + +For a complete list of available settings and their descriptions, refer to the `settings.py` file in `application/core`. Remember to restart your Docker containers after making changes to your `.env` file or `settings.py` for the changes to take effect. \ No newline at end of file diff --git a/docs/pages/Deploying/Hosting-the-app.mdx b/docs/pages/Deploying/Hosting-the-app.mdx new file mode 100644 index 00000000..4fb72ddd --- /dev/null +++ b/docs/pages/Deploying/Hosting-the-app.mdx @@ -0,0 +1,33 @@ +import { DeploymentCards } from '../../components/DeploymentCards'; + +# Deployment Guides + + diff --git a/docs/pages/Deploying/Kubernetes-Deploying.md b/docs/pages/Deploying/Kubernetes-Deploying.mdx similarity index 88% rename from docs/pages/Deploying/Kubernetes-Deploying.md rename to docs/pages/Deploying/Kubernetes-Deploying.mdx index b91ec343..8f1c8f7a 100644 --- a/docs/pages/Deploying/Kubernetes-Deploying.md +++ b/docs/pages/Deploying/Kubernetes-Deploying.mdx @@ -1,4 +1,10 @@ -# Self-hosting DocsGPT on Kubernetes +--- +title: Deploying DocsGPT on Kubernetes +description: Learn how to self-host DocsGPT on a Kubernetes cluster for scalable and robust deployments. +--- + +# Self-hosting DocsGPT + on Kubernetes This guide will walk you through deploying DocsGPT on Kubernetes. @@ -11,7 +17,7 @@ Ensure you have the following installed before proceeding: ## Folder Structure -The `k8s` folder contains the necessary deployment and service configuration files: +The `deployment/k8s` folder contains the necessary deployment and service configuration files: - `deployments/` - `services/` @@ -23,7 +29,7 @@ The `k8s` folder contains the necessary deployment and service configuration fil ```sh git clone https://github.com/arc53/DocsGPT.git - cd docsgpt/k8s + cd docsgpt/deployment/k8s ``` 2. **Configure Secrets (optional)** diff --git a/docs/pages/Deploying/Quickstart.md b/docs/pages/Deploying/Quickstart.md deleted file mode 100644 index a2bdc706..00000000 --- a/docs/pages/Deploying/Quickstart.md +++ /dev/null @@ -1,69 +0,0 @@ -## Launching Web App -**Note**: Make sure you have Docker installed - -**On macOS or Linux:** -Just run the following command: - -```bash -./setup.sh -``` - -This command will install all the necessary dependencies and provide you with an option to use our LLM API, download the local model or use OpenAI. - -If you prefer to follow manual steps, refer to this guide: - -1. Open and download this repository with - ```bash - git clone https://github.com/arc53/DocsGPT.git - ``` -2. Create a `.env` file in your root directory and set your `API_KEY` with your [OpenAI API key](https://platform.openai.com/account/api-keys). (optional in case you want to use OpenAI) -3. Run the following commands: - ```bash - docker-compose build && docker-compose up - ``` -4. Navigate to http://localhost:5173/. - -To stop, simply press **Ctrl + C**. - -**For WINDOWS:** - -To run the setup on Windows, you have two options: using the Windows Subsystem for Linux (WSL) or using Git Bash or Command Prompt. - -**Option 1: Using Windows Subsystem for Linux (WSL):** - -1. Install WSL if you haven't already. You can follow the official Microsoft documentation for installation: (https://learn.microsoft.com/en-us/windows/wsl/install). -2. After setting up WSL, open the WSL terminal. -3. Clone the repository and create the `.env` file: - ```bash - git clone https://github.com/arc53/DocsGPT.git - cd DocsGPT - echo "API_KEY=Yourkey" > .env - echo "VITE_API_STREAMING=true" >> .env - ``` -4. Run the following command to start the setup with Docker Compose: - ```bash - ./run-with-docker-compose.sh - ``` -6. Open your web browser and navigate to http://localhost:5173/. -7. To stop the setup, just press **Ctrl + C** in the WSL terminal - -**Option 2: Using Git Bash or Command Prompt (CMD):** - -1. Install Git for Windows if you haven't already. Download it from the official website: (https://gitforwindows.org/). -2. Open Git Bash or Command Prompt. -3. Clone the repository and create the `.env` file: - ```bash - git clone https://github.com/arc53/DocsGPT.git - cd DocsGPT - echo "API_KEY=Yourkey" > .env - echo "VITE_API_STREAMING=true" >> .env - ``` -4. Run the following command to start the setup with Docker Compose: - ```bash - ./run-with-docker-compose.sh - ``` -5. Open your web browser and navigate to http://localhost:5173/. -6. To stop the setup, just press **Ctrl + C** in the Git Bash or Command Prompt terminal. - -These steps should help you set up and run the project on Windows using either WSL or Git Bash/Command Prompt. -**Important:** Ensure that Docker is installed and properly configured on your Windows system for these steps to work. diff --git a/docs/pages/Deploying/Railway-Deploying.md b/docs/pages/Deploying/Railway.mdx similarity index 93% rename from docs/pages/Deploying/Railway-Deploying.md rename to docs/pages/Deploying/Railway.mdx index ab6064a9..89928ab2 100644 --- a/docs/pages/Deploying/Railway-Deploying.md +++ b/docs/pages/Deploying/Railway.mdx @@ -1,3 +1,7 @@ +--- +title: Hosting DocsGPT on Railway +description: Learn how to deploy your own DocsGPT instance on Railway with this step-by-step tutorial +--- # Self-hosting DocsGPT on Railway @@ -97,11 +101,11 @@ To save the file, press CTRL+X, then Y, and then ENTER. -Next, set the correct IP for the Backend by opening the docker-compose.yml file: +Next, set the correct IP for the Backend by opening the docker-compose.yaml file: -`nano docker-compose.yml` +`nano deployment/docker-compose.yaml` @@ -123,7 +127,7 @@ You're almost there! Now that all the necessary bits and pieces have been instal -`sudo docker-compose up -d` +`sudo docker compose -f deployment/docker-compose.yaml up -d` diff --git a/docs/pages/Deploying/_meta.json b/docs/pages/Deploying/_meta.json index 64cd77db..706300b3 100644 --- a/docs/pages/Deploying/_meta.json +++ b/docs/pages/Deploying/_meta.json @@ -1,18 +1,32 @@ { + "DocsGPT-Settings": { + "title": "⚙️ App Configuration", + "href": "/Deploying/DocsGPT-Settings" + }, + "Docker-Deploying": { + "title": "🛳️ Docker Setup", + "href": "/Deploying/Docker-Deploying" + }, + "Development-Environment": { + "title": "🛠️Development Environment", + "href": "/Deploying/Development-Environment" + }, + "Kubernetes-Deploying": { + "title": "☸️ Deploying on Kubernetes", + "href": "/Deploying/Kubernetes-Deploying" + }, "Hosting-the-app": { "title": "☁️ Hosting DocsGPT", "href": "/Deploying/Hosting-the-app" }, - "Quickstart": { - "title": "⚡️Quickstart", - "href": "/Deploying/Quickstart" + "Amazon-Lightsail": { + "title": "Hosting DocsGPT on Amazon Lightsail", + "href": "/Deploying/Amazon-Lightsail", + "display": "hidden" }, - "Railway-Deploying": { - "title": "🚂Deploying on Railway", - "href": "/Deploying/Railway-Deploying" - }, - "Kubernetes-Deploying": { - "title": "☸️Deploying on Kubernetes", - "href": "/Deploying/Kubernetes-Deploying" + "Railway": { + "title": "Hosting DocsGPT on Railway", + "href": "/Deploying/Railway", + "display": "hidden" } } \ No newline at end of file diff --git a/docs/pages/Extensions/Chatwoot-extension.md b/docs/pages/Extensions/Chatwoot-extension.mdx similarity index 71% rename from docs/pages/Extensions/Chatwoot-extension.md rename to docs/pages/Extensions/Chatwoot-extension.mdx index d6494bbf..68abc949 100644 --- a/docs/pages/Extensions/Chatwoot-extension.md +++ b/docs/pages/Extensions/Chatwoot-extension.mdx @@ -1,8 +1,12 @@ +--- +title: Comprehensive Guide to Setting Up the Chatwoot Extension with DocsGPT +description: This step-by-step guide walks you through the process of setting up the Chatwoot extension with DocsGPT, enabling seamless integration for automated responses and enhanced customer support. Learn how to launch DocsGPT, retrieve your Chatwoot access token, configure the .env file, and start the extension. +--- ## Chatwoot Extension Setup Guide ### Step 1: Prepare and Start DocsGPT -- **Launch DocsGPT**: Follow the instructions in our [DocsGPT Wiki](https://github.com/arc53/DocsGPT/wiki) to start DocsGPT. Make sure to load your documentation. +- **Launch DocsGPT**: Follow the instructions in our [Quickstart](/quickstart) to start DocsGPT. Make sure to load your documentation. ### Step 2: Get Access Token from Chatwoot diff --git a/docs/pages/Extensions/Chrome-extension.mdx b/docs/pages/Extensions/Chrome-extension.mdx index 2eb36ceb..69b741e6 100644 --- a/docs/pages/Extensions/Chrome-extension.mdx +++ b/docs/pages/Extensions/Chrome-extension.mdx @@ -1,3 +1,7 @@ +--- +title: Add DocsGPT Chrome Extension to Your Browser +description: Install the DocsGPT Chrome extension to access AI-powered document assistance directly from your browser for enhanced productivity. +--- import {Steps} from 'nextra/components' import { Callout } from 'nextra/components' diff --git a/docs/pages/Extensions/_meta.json b/docs/pages/Extensions/_meta.json index 270367de..2ad7ab0c 100644 --- a/docs/pages/Extensions/_meta.json +++ b/docs/pages/Extensions/_meta.json @@ -1,14 +1,22 @@ { - "Chatwoot-extension": { - "title": "💬️ Chatwoot Extension", - "href": "/Extensions/Chatwoot-extension" + "api-key-guide": { + "title": "🔑 Getting API key", + "href": "/Extensions/api-key-guide" }, - "react-widget": { - "title": "🏗️ Widget setup", - "href": "/Extensions/react-widget" + "chat-widget": { + "title": "💬️ Chat Widget", + "href": "/Extensions/chat-widget" + }, + "search-widget": { + "title": "🔎 Search Widget", + "href": "/Extensions/search-widget" }, "Chrome-extension": { "title": "🌐 Chrome Extension", "href": "/Extensions/Chrome-extension" + }, + "Chatwoot-extension": { + "title": "🗣️ Chatwoot Extension", + "href": "/Extensions/Chatwoot-extension" } } \ No newline at end of file diff --git a/docs/pages/API/api-key-guide.md b/docs/pages/Extensions/api-key-guide.mdx similarity index 60% rename from docs/pages/API/api-key-guide.md rename to docs/pages/Extensions/api-key-guide.mdx index 53bb4b58..f1b83633 100644 --- a/docs/pages/API/api-key-guide.md +++ b/docs/pages/Extensions/api-key-guide.mdx @@ -1,22 +1,20 @@ -## Guide to DocsGPT API Keys +--- +title: API Keys for DocsGPT Integrations +description: Learn how to obtain, understand, and use DocsGPT API keys to integrate DocsGPT into your external applications and widgets. +--- -DocsGPT API keys are essential for developers and users who wish to integrate the DocsGPT models into external applications, such as the our widget. This guide will walk you through the steps of obtaining an API key, starting from uploading your document to understanding the key variables associated with API keys. +# Guide to DocsGPT API Keys -### Uploading Your Document +DocsGPT API keys are essential for developers and users who wish to integrate the DocsGPT models into external applications, such as [our widget](/Extensions/chat-widget). This guide will walk you through the steps of obtaining an API key, starting from uploading your document to understanding the key variables associated with API keys. -Before creating your first API key, you must upload the document that will be linked to this key. You can upload your document through two methods: - -- **GUI Web App Upload:** A user-friendly graphical interface that allows for easy upload and management of documents. -- **Using `/api/upload` Method:** For users comfortable with API calls, this method provides a direct way to upload documents. - -### Obtaining Your API Key +## Obtaining Your API Key After uploading your document, you can obtain an API key either through the graphical user interface or via an API call: - **Graphical User Interface:** Navigate to the Settings section of the DocsGPT web app, find the API Keys option, and press 'Create New' to generate your key. -- **API Call:** Alternatively, you can use the `/api/create_api_key` endpoint to create a new API key. For detailed instructions, visit [DocsGPT API Documentation](https://docs.docsgpt.cloud/API/API-docs#8-apicreate_api_key). +- **API Call:** Alternatively, you can use the `/api/create_api_key` endpoint to create a new API key. For detailed instructions, visit [DocsGPT API Documentation](https://gptcloud.arc53.com/). -### Understanding Key Variables +## Understanding Key Variables Upon creating your API key, you will encounter several key variables. Each serves a specific purpose: @@ -27,4 +25,4 @@ Upon creating your API key, you will encounter several key variables. Each serve With your API key ready, you can now integrate DocsGPT into your application, such as the DocsGPT Widget or any other software, via `/api/answer` or `/stream` endpoints. The source document is preset with the API key, allowing you to bypass fields like `selectDocs` and `active_docs` during implementation. -Congratulations on taking the first step towards enhancing your applications with DocsGPT! With this guide, you're now equipped to navigate the process of obtaining and understanding DocsGPT API keys. +Congratulations on taking the first step towards enhancing your applications with DocsGPT! diff --git a/docs/pages/Extensions/chat-widget copy.md b/docs/pages/Extensions/chat-widget copy.md new file mode 100644 index 00000000..32a9fa67 --- /dev/null +++ b/docs/pages/Extensions/chat-widget copy.md @@ -0,0 +1,212 @@ +# Setting up the DocsGPT Widget in Your React Project + +## Introduction: +The DocsGPT Widget is a powerful tool that allows you to integrate AI-powered documentation assistance into your web applications. This guide will walk you through the installation and usage of the DocsGPT Widget in your React project. Whether you're building a web app or a knowledge base, this widget can enhance your user experience. + +## Installation +First, make sure you have Node.js and npm installed in your project. Then go to your project and install a new dependency: `npm install docsgpt`. + +## Usage +In the file where you want to use the widget, import it and include the CSS file: +```js +import { DocsGPTWidget } from "docsgpt"; +``` + + +Now, you can use the widget in your component like this : +```jsx + +``` +## Props Table for DocsGPT Widget + +| **Prop** | **Type** | **Default Value** | **Description** | +|--------------------|------------------|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------------| +| **`apiHost`** | `string` | `"https://gptcloud.arc53.com"` | The URL of your DocsGPT API for vector search and chatbot queries. | +| **`apiKey`** | `string` | `""` | Your API key for authentication. Can be left empty if authentication is not required. | +| **`avatar`** | `string` | `"https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"` | Specifies the URL of the avatar or image representing the chatbot. | +| **`title`** | `string` | `"Get AI assistance"` | Sets the title text displayed in the chatbot interface. | +| **`description`** | `string` | `"DocsGPT's AI Chatbot is here to help"` | Provides a brief description of the chatbot's purpose or functionality. | +| **`heroTitle`** | `string` | `"Welcome to DocsGPT !"` | Displays a welcome title when users interact with the chatbot. | +| **`heroDescription`** | `string` | `"This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources."` | Provides additional introductory text or information about the chatbot's capabilities. | +| **`theme`** | `"dark" \| "light"` | `"dark"` | Allows you to select the theme for the chatbot interface. Accepts `"dark"` or `"light"`. | +| **`buttonIcon`** | `string` | `"https://your-icon"` | Specifies the URL of the icon image for the widget's launch button. | +| **`buttonBg`** | `string` | `"#222327"` | Sets the background color of the widget's launch button. | +| **`size`** | `"small" \| "medium"` | `"medium"` | Sets the size of the widget. Options are `"small"` or `"medium"`. | + +--- + +## Notes +- **Customizing Props:** All properties can be overridden when embedding the widget. For example, you can provide a unique avatar, title, or color scheme to better align with your brand. +- **Default Theme:** The widget defaults to the dark theme unless explicitly set to `"light"`. +- **API Key:** If the `apiKey` is not required for your application, leave it empty. + +This table provides a clear overview of the customization options available for tailoring the DocsGPT widget to fit your application. + + +## How to use DocsGPTWidget with [Nextra](https://nextra.site/) (Next.js + MDX) +Install your widget as described above and then go to your `pages/` folder and create a new file `_app.js` with the following content: +```js +import { DocsGPTWidget } from "docsgpt"; + +export default function MyApp({ Component, pageProps }) { + return ( + <> + + + + ) +} +``` +## How to use DocsGPTWidget with HTML +```html + + + + + + + HTML + CSS + + + +

This is a simple HTML + CSS template!

+
+ + + + + +``` +To link the widget to your api and your documents you can pass parameters to the renderDocsGPTWidget('div id', { parameters }). +```html + + + + + + + DocsGPT Widget + + + +
+ + + + +``` + +# SearchBar + +The `SearchBar` component is an interactive search bar designed to provide search results based on **vector similarity search**. It also includes the capability to open the AI Chatbot, enabling users to query. + +--- + +### Importing the Component +```tsx +import { SearchBar } from "docsgpt-react"; +``` + +--- + +### Usage Example +```tsx + +``` + +--- + +## HTML embedding for Search bar + +```html + + + + + + SearchBar Embedding + + + + +
+ + + + + +``` + +### Props + +| **Prop** | **Type** | **Default Value** | **Description** | +|-----------------|-----------|-------------------------------------|--------------------------------------------------------------------------------------------------| +| **`apiKey`** | `string` | `"74039c6d-bff7-44ce-ae55-2973cbf13837"` | Your API key generated from the app. Used for authenticating requests. | +| **`apiHost`** | `string` | `"https://gptcloud.arc53.com"` | The base URL of the server hosting the vector similarity search and chatbot services. | +| **`theme`** | `"dark" \| "light"` | `"dark"` | The theme of the search bar. Accepts `"dark"` or `"light"`. | +| **`placeholder`** | `string` | `"Search or Ask AI..."` | Placeholder text displayed in the search input field. | +| **`width`** | `string` | `"256px"` | Width of the search bar. Accepts any valid CSS width value (e.g., `"300px"`, `"100%"`, `"20rem"`). | + + +Feel free to reach out if you need help customizing or extending the `SearchBar`! + +## Our github + +[DocsGPT](https://github.com/arc53/DocsGPT) + +You can find the source code in the extensions/react-widget folder. + +For more information about React, refer to this [link here](https://react.dev/learn) + diff --git a/docs/pages/Extensions/chat-widget.mdx b/docs/pages/Extensions/chat-widget.mdx new file mode 100644 index 00000000..4cc887dc --- /dev/null +++ b/docs/pages/Extensions/chat-widget.mdx @@ -0,0 +1,158 @@ +--- +title: Integrate DocsGPT Chat Widget into Your Web Application +description: Embed the DocsGPT Widget in your React, HTML, or Nextra projects to provide AI-powered chat functionality to your users. +--- +import { Tabs } from 'nextra/components' + +# Integrating DocsGPT Chat Widget + +## Introduction + +The DocsGPT Widget is a powerful tool that allows you to integrate AI-driven document assistance directly into your web applications. This guide will walk you through embedding the DocsGPT Widget into your projects, whether you're using React, plain HTML, or Nextra. Enhance your user experience by providing seamless access to intelligent document search and chatbot capabilities. + +Try out the interactive widget showcase and customize its parameters at the [DocsGPT Widget Demo](https://widget.docsgpt.cloud/). + +## Setup + + + +### Installation + +Make sure you have Node.js and npm (or yarn, pnpm) installed in your project. Navigate to your project directory in the terminal and install the `docsgpt` package: + +```bash npm +npm install docsgpt +``` + +### Usage + +In your React component file, import the `DocsGPTWidget` component: + +```js +import { DocsGPTWidget } from "docsgpt"; +``` + +Now, you can embed the widget within your React component's JSX: + +```jsx + +``` + + + +### Installation + +To use the DocsGPT Widget directly in HTML, include the widget script from a CDN in your HTML file: + +```html filename="html" + +``` + +### Usage + +In your HTML ``, add a `
` element where you want to render the widget. Set an `id` for easy targeting. + +```html filename="html" +
+``` + +Then, in a ` +``` + + + + +### Installation + +Make sure you have Node.js and npm (or yarn, pnpm) installed in your project. Navigate to your project directory in the terminal and install the `docsgpt` package: + +```bash npm +npm install docsgpt +``` + +### Usage with Nextra (Next.js + MDX) + +To integrate the DocsGPT Widget into a [Nextra](https://nextra.site/) documentation site (built with Next.js and MDX), create or modify your `pages/_app.js` file as follows: + +```js filename="pages/_app.js" +import { DocsGPTWidget } from "docsgpt"; + +export default function MyApp({ Component, pageProps }) { + return ( + <> + + + + ) +} +``` + + + +--- + +## Properties Table + +The DocsGPT Widget offers a range of customizable properties that allow you to tailor its appearance and behavior to perfectly match your web application. These parameters can be modified directly when embedding the widget in your React components or HTML code. Below is a detailed overview of each available prop: + +| **Prop** | **Type** | **Default Value** | **Description** | +|--------------------|------------------|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------------| +| **`apiHost`** | `string` | `"https://gptcloud.arc53.com"` | **Required.** The URL of your DocsGPT API backend. This endpoint handles vector search and chatbot queries. | +| **`apiKey`** | `string` | `"your-api-key"` | API key for authentication with your DocsGPT API. Leave empty if no authentication is required. | +| **`avatar`** | `string` | [`dino-icon-link`](https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png) | URL for the avatar image displayed in the chatbot interface. | +| **`title`** | `string` | `"Get AI assistance"` | Title text shown in the chatbot header. | +| **`description`** | `string` | `"DocsGPT's AI Chatbot is here to help"` | Sub-title or descriptive text displayed below the title in the chatbot header. | +| **`heroTitle`** | `string` | `"Welcome to DocsGPT !"` | Welcome message displayed when the chatbot is initially opened. | +| **`heroDescription`** | `string` | `"This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources."` | Introductory text providing context or disclaimers about the chatbot. | +| **`theme`** | `"dark" \| "light"` | `"dark"` | Color theme of the widget interface. Options: `"dark"` or `"light"`. Defaults to `"dark"`. | +| **`buttonIcon`** | `string` | `"https://your-icon"` | URL for the icon image used in the widget's launch button. | +| **`buttonBg`** | `string` | `"#222327"` | Background color of the widget's launch button. | +| **`size`** | `"small" \| "medium"` | `"medium"` | Size of the widget. Options: `"small"` or `"medium"`. Defaults to `"medium"`. | + +--- + +## Notes on Widget Properties + +* **Full Customization:** Every property listed in the table can be customized. Override the defaults to create a widget that perfectly matches your branding and application context. From avatars and titles to color schemes, you have fine-grained control over the widget's presentation. +* **API Key Handling:** The `apiKey` prop is optional. Only include it if your DocsGPT backend API is configured to require API key authentication. `apiHost` for DocsGPT Cloud is `https://gptcloud.arc53.com/` + +## Explore and Customize Further + +The DocsGPT Widget is fully open-source, allowing for deep customization and extension beyond the readily available props. + +The complete source code for the React-based widget is available in the `extensions/react-widget` directory within the main [DocsGPT GitHub Repository](https://github.com/arc53/DocsGPT). Feel free to explore the code, fork the repository, and tailor the widget to your exact requirements. \ No newline at end of file diff --git a/docs/pages/Extensions/react-widget.md b/docs/pages/Extensions/react-widget.md deleted file mode 100644 index 1859b558..00000000 --- a/docs/pages/Extensions/react-widget.md +++ /dev/null @@ -1,125 +0,0 @@ -### Setting up the DocsGPT Widget in Your React Project - -### Introduction: -The DocsGPT Widget is a powerful tool that allows you to integrate AI-powered documentation assistance into your web applications. This guide will walk you through the installation and usage of the DocsGPT Widget in your React project. Whether you're building a web app or a knowledge base, this widget can enhance your user experience. - -### Installation -First, make sure you have Node.js and npm installed in your project. Then go to your project and install a new dependency: `npm install docsgpt`. - -### Usage -In the file where you want to use the widget, import it and include the CSS file: -```js -import { DocsGPTWidget } from "docsgpt"; -``` - - -Now, you can use the widget in your component like this : -```jsx - -``` -To tailor the widget to your needs, you can configure the following props in your component: -1. `apiHost` — The URL of your DocsGPT API. -2. `theme` — Allows to select your specific theme (dark or light). -3. `apiKey` — Usually, it's empty. -4. `avatar`: Specifies the URL of the avatar or image representing the chatbot. -5. `title`: Sets the title text displayed in the chatbot interface. -6. `description`: Provides a brief description of the chatbot's purpose or functionality. -7. `heroTitle`: Displays a welcome title when users interact with the chatbot. -8. `heroDescription`: Provide additional introductory text or information about the chatbot's capabilities. -9. `buttonIcon`: Specifies the url of the icon image for the widget. -10. `buttonBg`: Allows to specify the Background color of the widget. -11. `size`: Sets the size of the widget ( small, medium). - - -### How to use DocsGPTWidget with [Nextra](https://nextra.site/) (Next.js + MDX) -Install your widget as described above and then go to your `pages/` folder and create a new file `_app.js` with the following content: -```js -import { DocsGPTWidget } from "docsgpt"; - -export default function MyApp({ Component, pageProps }) { - return ( - <> - - - - ) -} -``` -### How to use DocsGPTWidget with HTML -```html - - - - - - - HTML + CSS - - - -

This is a simple HTML + CSS template!

-
- - - - - -``` -To link the widget to your api and your documents you can pass parameters to the renderDocsGPTWidget('div id', { parameters }). -```html - - - - - - - DocsGPT Widget - - - -
- - - - -``` - -For more information about React, refer to this [link here](https://react.dev/learn) - diff --git a/docs/pages/Extensions/search-widget.mdx b/docs/pages/Extensions/search-widget.mdx new file mode 100644 index 00000000..80db407c --- /dev/null +++ b/docs/pages/Extensions/search-widget.mdx @@ -0,0 +1,116 @@ +--- +title: Integrate DocsGPT Search Bar into Your Web Application +description: Embed the DocsGPT Search Bar Widget in your React or HTML projects to provide AI-powered document search functionality to your users. +--- +import { Tabs } from 'nextra/components' + +# Integrating DocsGPT Search Bar Widget + +## Introduction + +The DocsGPT Search Bar Widget offers a simple yet powerful way to embed AI-powered document search directly into your web applications. This widget allows users to perform searches across your documents or pages, enabling them to quickly find the information they need. This guide will walk you through embedding the Search Bar Widget into your projects, whether you're using React or plain HTML. + +Try out the interactive widget showcase and customize its parameters at the [DocsGPT Widget Demo](https://widget.docsgpt.cloud/). + +## Setup + + + +## React Setup + +### Installation + +Make sure you have Node.js and npm (or yarn, pnpm) installed in your project. Navigate to your project directory in the terminal and install the `docsgpt` package: + +```bash npm +npm install docsgpt +``` + +### Usage + +In your React component file, import the `SearchBar` component: + +```js +import { SearchBar } from "docsgpt"; +``` + +Now, you can embed the widget within your React component's JSX: + +```jsx + +``` + + + +### Installation + +To use the DocsGPT Search Bar Widget directly in HTML, include the widget script from a CDN in your HTML file: + +```html filename="html" + +``` + +### Usage + +In your HTML ``, add a `
` element where you want to render the Search Bar Widget. Set an `id` for easy targeting. + +```html filename="html" +
+``` + +Then, in a ` +``` + + + + +--- + +## Properties Table + +The DocsGPT Search Bar Widget offers a range of customizable properties that allow you to tailor its appearance and behavior to perfectly match your web application. These parameters can be modified directly when embedding the widget in your React components or HTML code. Below is a detailed overview of each available prop: + +| **Prop** | **Type** | **Default Value** | **Description** | +|-----------------|-----------|-------------------------------------|--------------------------------------------------------------------------------------------------| +| **`apiKey`** | `string` | `"your-api-key"` | API key for authentication with your DocsGPT API. Leave empty if no authentication is required. | +| **`apiHost`** | `string` | `"https://gptcloud.arc53.com"` | **Required.** The URL of your DocsGPT API backend. This endpoint handles vector similarity search queries. | +| **`theme`** | `"dark" \| "light"` | `"dark"` | Color theme of the search bar. Options: `"dark"` or `"light"`. Defaults to `"dark"`. | +| **`placeholder`** | `string` | `"Search or Ask AI..."` | Placeholder text displayed in the search input field. | +| **`width`** | `string` | `"256px"` | Width of the search bar. Accepts any valid CSS width value (e.g., `"300px"`, `"100%"`, `"20rem"`). | + +--- + +## Notes on Widget Properties + +* **Full Customization:** Every property listed in the table can be customized. Override the defaults to create a Search Bar Widget that perfectly matches your branding and application context. +* **API Key Handling:** The `apiKey` prop is optional. Only include it if your DocsGPT backend API is configured to require API key authentication. `apiHost` for DocsGPT Cloud is `https://gptcloud.arc53.com/` + +## Explore and Customize Further + +The DocsGPT Search Bar Widget is fully open-source, allowing for deep customization and extension beyond the readily available props. + +The complete source code for the React-based widget is available in the `extensions/react-widget` directory within the main [DocsGPT GitHub Repository](https://github.com/arc53/DocsGPT). Feel free to explore the code, fork the repository, and tailor the widget to your exact requirements. \ No newline at end of file diff --git a/docs/pages/Guides/Customising-prompts.mdx b/docs/pages/Guides/Customising-prompts.mdx index 41be967d..a0032a00 100644 --- a/docs/pages/Guides/Customising-prompts.mdx +++ b/docs/pages/Guides/Customising-prompts.mdx @@ -1,3 +1,8 @@ +--- +title: Customizing Prompts +description: This guide will explain how to change prompts in DocsGPT and why it might be benefitial. Additionaly this article expains additional variables that can be used in prompts. +--- + import Image from 'next/image' # Customizing the Main Prompt @@ -34,6 +39,8 @@ When using code examples, use the following format: {summaries} ``` +Note that `{summaries}` allows model to see and respond to your upploaded documents. If you don't want this functionality you can safely remove it from the customized prompt. + Feel free to customize the prompt to align it with your specific use case or the kind of responses you want from the AI. For example, you can focus on specific document types, industries, or topics to get more targeted results. ## Conclusion diff --git a/docs/pages/Guides/How-to-train-on-other-documentation.mdx b/docs/pages/Guides/How-to-train-on-other-documentation.mdx index f0149618..4e11d6fa 100644 --- a/docs/pages/Guides/How-to-train-on-other-documentation.mdx +++ b/docs/pages/Guides/How-to-train-on-other-documentation.mdx @@ -1,3 +1,7 @@ +--- +title: How to Train on Other Documentation +description: A step-by-step guide on how to effectively train DocsGPT on additional documentation sources. +--- import { Callout } from 'nextra/components' import Image from 'next/image' diff --git a/docs/pages/Guides/How-to-use-different-LLM.mdx b/docs/pages/Guides/How-to-use-different-LLM.mdx index c867fdcc..3bc8477d 100644 --- a/docs/pages/Guides/How-to-use-different-LLM.mdx +++ b/docs/pages/Guides/How-to-use-different-LLM.mdx @@ -1,3 +1,7 @@ +--- +title: +description: +--- import { Callout } from 'nextra/components' import Image from 'next/image' @@ -26,24 +30,13 @@ Choose the LLM of your choice. prompts ### For Open source llm change: - + ### Step 1 -For open source you have to edit .env file with LLM_NAME with their desired LLM name. +For open source version please edit `LLM_NAME`, `MODEL_NAME` and others in the .env file. Refer to [⚙️ App Configuration](/Deploying/DocsGPT-Settings) for more information. ### Step 2 -All the supported LLM providers are here application/llm and you can check what env variable are needed for each -List of latest supported LLMs are https://github.com/arc53/DocsGPT/blob/main/application/llm/llm_creator.py -### Step 3 -Visit application/llm and select the file of your selected llm and there you will find the specific requirements needed to be filled in order to use it,i.e API key of that llm. +Visit [☁️ Cloud Providers](/Models/cloud-providers) for the updated list of online models. Make sure you have the right API_KEY and correct LLM_NAME. +For self-hosted please visit [🖥️ Local Inference](/Models/local-inference). -### For OpenAI-Compatible Endpoints: -DocsGPT supports the use of OpenAI-compatible endpoints through base URL substitution. This feature allows you to use alternative AI models or services that implement the OpenAI API interface. - - -Set the OPENAI_BASE_URL in your environment. You can change .env file with OPENAI_BASE_URL with the desired base URL or docker-compose.yml file and add the environment variable to the backend container. - -> Make sure you have the right API_KEY and correct LLM_NAME. - - diff --git a/docs/pages/Guides/My-AI-answers-questions-using-external-knowledge.md b/docs/pages/Guides/My-AI-answers-questions-using-external-knowledge.mdx similarity index 95% rename from docs/pages/Guides/My-AI-answers-questions-using-external-knowledge.md rename to docs/pages/Guides/My-AI-answers-questions-using-external-knowledge.mdx index 99e3c757..318bf41b 100644 --- a/docs/pages/Guides/My-AI-answers-questions-using-external-knowledge.md +++ b/docs/pages/Guides/My-AI-answers-questions-using-external-knowledge.mdx @@ -1,3 +1,8 @@ +--- +title: +description: +--- + # Avoiding hallucinations If your AI uses external knowledge and is not explicit enough, it is ok, because we try to make DocsGPT friendly. diff --git a/docs/pages/Guides/_meta.json b/docs/pages/Guides/_meta.json index 454670fc..1a331167 100644 --- a/docs/pages/Guides/_meta.json +++ b/docs/pages/Guides/_meta.json @@ -9,10 +9,12 @@ }, "How-to-use-different-LLM": { "title": "️🤖 How to use different LLM's", - "href": "/Guides/How-to-use-different-LLM" + "href": "/Guides/How-to-use-different-LLM", + "display": "hidden" }, "My-AI-answers-questions-using-external-knowledge": { "title": "💭️ Avoiding hallucinations", - "href": "/Guides/My-AI-answers-questions-using-external-knowledge" + "href": "/Guides/My-AI-answers-questions-using-external-knowledge", + "display": "hidden" } } \ No newline at end of file diff --git a/docs/pages/Models/_meta.json b/docs/pages/Models/_meta.json new file mode 100644 index 00000000..d1256cd1 --- /dev/null +++ b/docs/pages/Models/_meta.json @@ -0,0 +1,14 @@ +{ + "cloud-providers": { + "title": "☁️ Cloud Providers", + "href": "/Models/cloud-providers" + }, + "local-inference": { + "title": "🖥️ Local Inference", + "href": "/Models/local-inference" + }, + "embeddings": { + "title": "📝 Embeddings", + "href": "/Models/embeddings" + } +} \ No newline at end of file diff --git a/docs/pages/Models/cloud-providers.mdx b/docs/pages/Models/cloud-providers.mdx new file mode 100644 index 00000000..86f2d132 --- /dev/null +++ b/docs/pages/Models/cloud-providers.mdx @@ -0,0 +1,55 @@ +--- +title: Connecting DocsGPT to Cloud LLM Providers +description: Connect DocsGPT to various Cloud Large Language Model (LLM) providers to power your document Q&A. +--- + +# Connecting DocsGPT to Cloud LLM Providers + +DocsGPT is designed to seamlessly integrate with a variety of Cloud Large Language Model (LLM) providers, giving you access to state-of-the-art AI models for document question answering. + +## Configuration via `.env` file + +The primary method for configuring your LLM provider in DocsGPT is through the `.env` file. For a comprehensive understanding of all available settings, please refer to the detailed [DocsGPT Settings Guide](/Deploying/DocsGPT-Settings). + +To connect to a cloud LLM provider, you will typically need to configure the following basic settings in your `.env` file: + +* **`LLM_NAME`**: This setting is essential and identifies the specific cloud provider you wish to use (e.g., `openai`, `google`, `anthropic`). +* **`MODEL_NAME`**: Specifies the exact model you want to utilize from your chosen provider (e.g., `gpt-4o`, `gemini-2.0-flash`, `claude-3-5-sonnet-latest`). Refer to your provider's documentation for a list of available models. +* **`API_KEY`**: Almost all cloud LLM providers require an API key for authentication. Obtain your API key from your chosen provider's platform and securely store it in your `.env` file. + +## Explicitly Supported Cloud Providers + +DocsGPT offers direct, streamlined support for the following cloud LLM providers, making configuration straightforward. The table below outlines the `LLM_NAME` and example `MODEL_NAME` values to use for each provider in your `.env` file. + +| Provider | `LLM_NAME` | Example `MODEL_NAME` | +| :--------------------------- | :------------- | :-------------------------- | +| DocsGPT Public API | `docsgpt` | `None` | +| OpenAI | `openai` | `gpt-4o` | +| Google (Vertex AI, Gemini) | `google` | `gemini-2.0-flash` | +| Anthropic (Claude) | `anthropic` | `claude-3-5-sonnet-latest` | +| Groq | `groq` | `llama-3.1-8b-instant` | +| HuggingFace Inference API | `huggingface` | `meta-llama/Llama-3.1-8B-Instruct` | +| Azure OpenAI | `azure_openai` | `gpt-4o` | + +## Connecting to OpenAI-Compatible Cloud APIs + +DocsGPT's flexible architecture allows you to connect to any cloud provider that offers an API compatible with the OpenAI API standard. This opens up a vast ecosystem of LLM services. + +To connect to an OpenAI-compatible cloud provider, you will still use `LLM_NAME=openai` in your `.env` file. However, you will also need to specify the API endpoint of your chosen provider using the `OPENAI_BASE_URL` setting. You will also likely need to provide an `API_KEY` and `MODEL_NAME` as required by that provider. + +**Example for DeepSeek (OpenAI-Compatible API):** + +To connect to DeepSeek, which offers an OpenAI-compatible API, your `.env` file could be configured as follows: + +``` +LLM_NAME=openai +API_KEY=YOUR_API_KEY # Your DeepSeek API key +MODEL_NAME=deepseek-chat # Or your desired DeepSeek model name +OPENAI_BASE_URL=https://api.deepseek.com/v1 # DeepSeek's OpenAI API URL +``` + +Remember to consult the documentation of your chosen OpenAI-compatible cloud provider for their specific API endpoint, required model names, and authentication methods. + +## Adding Support for Other Cloud Providers + +If you wish to connect to a cloud provider that is not explicitly listed above or doesn't offer OpenAI API compatibility, you can extend DocsGPT to support it. Within the DocsGPT repository, navigate to the `application/llm` directory. Here, you will find Python files defining the existing LLM integrations. You can use these files as examples to create a new module for your desired cloud provider. After creating your new LLM module, you will need to register it within the `llm_creator.py` file. This process involves some coding, but it allows for virtually unlimited extensibility to connect to any cloud-based LLM service with an accessible API. \ No newline at end of file diff --git a/docs/pages/Models/embeddings.md b/docs/pages/Models/embeddings.md new file mode 100644 index 00000000..6dfb89b6 --- /dev/null +++ b/docs/pages/Models/embeddings.md @@ -0,0 +1,72 @@ +--- +title: Understanding and Configuring Embedding Models in DocsGPT +description: Learn about embedding models, their importance in DocsGPT, and how to configure them for optimal performance. +--- + +# Understanding and Configuring Embedding Models in DocsGPT + +Embedding models are a crucial component of DocsGPT, enabling its powerful document understanding and question-answering capabilities. This guide will explain what embedding models are, why they are essential for DocsGPT, and how to configure them. + +## What are Embedding Models? + +In simple terms, an embedding model is a type of language model that converts text into numerical vectors. These vectors, known as embeddings, capture the semantic meaning of the text. Think of it as translating words and sentences into a language that computers can understand mathematically, where similar meanings are represented by vectors that are close to each other in vector space. + +**Why are embedding models important for DocsGPT?** + +DocsGPT uses embedding models for several key tasks: + +* **Semantic Search:** When you upload documents to DocsGPT, the application uses an embedding model to generate embeddings for each document chunk. These embeddings are stored in a vector store. When you ask a question, your query is also converted into an embedding. DocsGPT then performs a semantic search in the vector store, finding document chunks whose embeddings are most similar to your query embedding. This allows DocsGPT to retrieve relevant information based on the *meaning* of your question and documents, not just keyword matching. +* **Document Understanding:** Embeddings help DocsGPT understand the underlying meaning of your documents, enabling it to answer questions accurately and contextually, even if the exact keywords from your question are not present in the retrieved document chunks. + +In essence, embedding models are the bridge that allows DocsGPT to understand the nuances of human language and connect your questions to the relevant information within your documents. + +## Out-of-the-Box Embedding Model Support in DocsGPT + +DocsGPT is designed to be flexible and supports a wide range of embedding models right out of the box. Currently, DocsGPT provides native support for models from two major sources: + +* **Sentence Transformers:** DocsGPT supports all models available through the [Sentence Transformers library](https://www.sbert.net/). This library offers a vast selection of pre-trained embedding models, known for their quality and efficiency in various semantic tasks. +* **OpenAI Embeddings:** DocsGPT also supports using embedding models from OpenAI, specifically the `text-embedding-ada-002` model, which is a powerful and widely used embedding model from OpenAI's API. + +## Configuring Sentence Transformer Models + +To utilize Sentence Transformer models within DocsGPT, you need to follow these steps: + +1. **Download the Model:** Sentence Transformer models are typically hosted on Hugging Face Model Hub. You need to download your chosen model and place it in the `model/` folder in the root directory of your DocsGPT project. + + For example, to use the `all-mpnet-base-v2` model, you would set `EMBEDDINGS_NAME` as described below, and ensure that the model files are available locally (DocsGPT will attempt to download it if it's not found, but local download is recommended for development and offline use). + +2. **Set `EMBEDDINGS_NAME` in `.env` (or `settings.py`):** You need to configure the `EMBEDDINGS_NAME` setting in your `.env` file (or `settings.py`) to point to the desired Sentence Transformer model. + + * **Using a pre-downloaded model from `model/` folder:** You can specify a path to the downloaded model within the `model/` directory. For instance, if you downloaded `all-mpnet-base-v2` and it's in `model/all-mpnet-base-v2`, you could potentially use a relative path like (though direct path to the model name is usually sufficient): + + ``` + EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2 + ``` + or simply use the model identifier: + ``` + EMBEDDINGS_NAME=sentence-transformers/all-mpnet-base-v2 + ``` + + * **Using a model directly from Hugging Face Model Hub:** You can directly specify the model identifier from Hugging Face Model Hub: + + ``` + EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2 + ``` + +## Using OpenAI Embeddings + +To use OpenAI's `text-embedding-ada-002` embedding model, you need to set `EMBEDDINGS_NAME` to `openai_text-embedding-ada-002` and ensure you have your OpenAI API key configured correctly via `API_KEY` in your `.env` file (if you are not using Azure OpenAI). + +**Example `.env` configuration for OpenAI Embeddings:** + +``` +LLM_NAME=openai +API_KEY=YOUR_OPENAI_API_KEY # Your OpenAI API Key +EMBEDDINGS_NAME=openai_text-embedding-ada-002 +``` + +## Adding Support for Other Embedding Models + +If you wish to use an embedding model that is not supported out-of-the-box, a good starting point for adding custom embedding model support is to examine the `base.py` file located in the `application/vectorstore` directory. + +Specifically, pay attention to the `EmbeddingsWrapper` and `EmbeddingsSingleton` classes. `EmbeddingsWrapper` provides a way to wrap different embedding model libraries into a consistent interface for DocsGPT. `EmbeddingsSingleton` manages the instantiation and retrieval of embedding model instances. By understanding these classes and the existing embedding model implementations, you can create your own custom integration for virtually any embedding model library you desire. \ No newline at end of file diff --git a/docs/pages/Models/local-inference.mdx b/docs/pages/Models/local-inference.mdx new file mode 100644 index 00000000..4aa6bca2 --- /dev/null +++ b/docs/pages/Models/local-inference.mdx @@ -0,0 +1,44 @@ +--- +title: Connecting DocsGPT to Local Inference Engines +description: Connect DocsGPT to local inference engines for running LLMs directly on your hardware. +--- + +# Connecting DocsGPT to Local Inference Engines + +DocsGPT can be configured to leverage local inference engines, allowing you to run Large Language Models directly on your own infrastructure. This approach offers enhanced privacy and control over your LLM processing. + +Currently, DocsGPT primarily supports local inference engines that are compatible with the OpenAI API format. This means you can connect DocsGPT to various local LLM servers that mimic the OpenAI API structure. + +## Configuration via `.env` file + +Setting up a local inference engine with DocsGPT is configured through environment variables in the `.env` file. For a detailed explanation of all settings, please consult the [DocsGPT Settings Guide](/Deploying/DocsGPT-Settings). + +To connect to a local inference engine, you will generally need to configure these settings in your `.env` file: + +* **`LLM_NAME`**: Crucially set this to `openai`. This tells DocsGPT to use the OpenAI-compatible API format for communication, even though the LLM is local. +* **`MODEL_NAME`**: Specify the model name as recognized by your local inference engine. This might be a model identifier or left as `None` if the engine doesn't require explicit model naming in the API request. +* **`OPENAI_BASE_URL`**: This is essential. Set this to the base URL of your local inference engine's API endpoint. This tells DocsGPT where to find your local LLM server. +* **`API_KEY`**: Generally, for local inference engines, you can set `API_KEY=None` as authentication is usually not required in local setups. + +## Supported Local Inference Engines (OpenAI API Compatible) + +DocsGPT is readily configurable to work with the following local inference engines, all communicating via the OpenAI API format. Here are example `OPENAI_BASE_URL` values for each, based on default setups: + +| Inference Engine | `LLM_NAME` | `OPENAI_BASE_URL` | +| :---------------------------- | :--------- | :------------------------- | +| LLaMa.cpp | `openai` | `http://localhost:8000/v1` | +| Ollama | `openai` | `http://localhost:11434/v1` | +| Text Generation Inference (TGI)| `openai` | `http://localhost:8080/v1` | +| SGLang | `openai` | `http://localhost:30000/v1` | +| vLLM | `openai` | `http://localhost:8000/v1` | +| Aphrodite | `openai` | `http://localhost:2242/v1` | +| FriendliAI | `openai` | `http://localhost:8997/v1` | +| LMDeploy | `openai` | `http://localhost:23333/v1` | + +**Important Note on `localhost` vs `host.docker.internal`:** + +The `OPENAI_BASE_URL` examples above use `http://localhost`. If you are running DocsGPT within Docker and your local inference engine is running on your host machine (outside of Docker), you will likely need to replace `localhost` with `http://host.docker.internal` to ensure Docker can correctly access your host's services. For example, `http://host.docker.internal:11434/v1` for Ollama. + +## Adding Support for Other Local Engines + +While DocsGPT currently focuses on OpenAI API compatible local engines, you can extend its capabilities to support other local inference solutions. To do this, navigate to the `application/llm` directory in the DocsGPT repository. Examine the existing Python files for examples of LLM integrations. You can create a new module for your desired local engine, and then register it in the `llm_creator.py` file within the same directory. This allows for custom integration with a wide range of local LLM servers beyond those listed above. \ No newline at end of file diff --git a/docs/pages/_app.mdx b/docs/pages/_app.mdx index 1cb8cadd..0111cd96 100644 --- a/docs/pages/_app.mdx +++ b/docs/pages/_app.mdx @@ -1,4 +1,4 @@ -import { DocsGPTWidget } from "docsgpt"; +import { DocsGPTWidget } from "docsgpt-react"; export default function MyApp({ Component, pageProps }) { return ( diff --git a/docs/pages/_meta.json b/docs/pages/_meta.json new file mode 100644 index 00000000..000b569d --- /dev/null +++ b/docs/pages/_meta.json @@ -0,0 +1,18 @@ + +{ + "index": "Home", + "quickstart": "Quickstart", + "Deploying": "Deploying", + "Models": "Models", + "Extensions": "Extensions", + "https://gptcloud.arc53.com/": { + "title": "API", + "href": "https://gptcloud.arc53.com/", + "newWindow": true + }, + "Guides": "Guides", + "changelog": { + "title": "Changelog", + "display": "hidden" + } +} \ No newline at end of file diff --git a/docs/pages/changelog.mdx b/docs/pages/changelog.mdx new file mode 100644 index 00000000..504854f3 --- /dev/null +++ b/docs/pages/changelog.mdx @@ -0,0 +1,3 @@ +--- +title: 'Changelog' +--- \ No newline at end of file diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index eedc2b09..1163f0a6 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -1,35 +1,87 @@ --- title: 'Home' +description: Documentation of DocsGPT - quickstart, deployment guides, model configuration, and widget integration documentation. --- import { Cards, Card } from 'nextra/components' import Image from 'next/image' -import deployingGuides from './Deploying/_meta.json'; -import developingGuides from './API/_meta.json'; -import extensionGuides from './Extensions/_meta.json'; -import mainGuides from './Guides/_meta.json'; - - - - export const allGuides = { - ...deployingGuides, - ...developingGuides, - ...extensionGuides, - ...mainGuides, + "quickstart": { + "title": "⚡️ Quickstart", + "href": "/quickstart" + }, + "DocsGPT-Settings": { + "title": "⚙️ App Configuration", + "href": "/Deploying/DocsGPT-Settings" + }, + "Docker-Deploying": { + "title": "🛳️ Docker Setup", + "href": "/Deploying/Docker-Deploying" + }, + "Development-Environment": { + "title": "🛠️Development Environment", + "href": "/Deploying/Development-Environment" + }, + "https://gptcloud.arc53.com/": { + "title": "🧑‍💻️ API", + "href": "https://gptcloud.arc53.com/", + "newWindow": true + }, + "cloud-providers": { + "title": "☁️ Cloud Providers", + "href": "/Models/cloud-providers" + }, + "local-inference": { + "title": "🖥️ Local Inference", + "href": "/Models/local-inference" + }, + "embeddings": { + "title": "📝 Embeddings", + "href": "/Models/embeddings" + }, + "api-key-guide": { + "title": "🔑 Getting API key", + "href": "/Extensions/api-key-guide" + }, + "chat-widget": { + "title": "💬️ Chat Widget", + "href": "/Extensions/chat-widget" + }, + "search-widget": { + "title": "🔎 Search Widget", + "href": "/Extensions/search-widget" + }, + "Customising-prompts": { + "title": "️💻 Customising Prompts", + "href": "/Guides/Customising-prompts" + } }; -### **DocsGPT 🦖** +# **DocsGPT 🦖** -DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖. Eliminate lengthy manual searches 🔍 and enhance your documentation experience with DocsGPT, and consider contributing to its AI-powered future 🚀. +DocsGPT is an open-source genAI tool that helps users get reliable answers from any knowledge source, while avoiding hallucinations. It enables quick and reliable information retrieval, with tooling and agentic system capability built in. - -homedemo + Try it yourself: [https://www.docsgpt.cloud/](https://www.docsgpt.cloud/) +### Features: +- **🗂️ Wide Format Support:** Reads PDF, DOCX, CSV, XLSX, EPUB, MD, RST, HTML, MDX, JSON, PPTX, and images. +- **🌐 Web & Data Integration:** Ingests from URLs, sitemaps, Reddit, GitHub and web crawlers. +- **✅ Reliable Answers:** Get accurate, hallucination-free responses with source citations viewable in a clean UI. +- **🔑 Streamlined API Keys:** Generate keys linked to your settings, documents, and models, simplifying chatbot and integration setup. +- **🔗 Actionable Tooling:** Connect to APIs, tools, and other services to enable LLM actions. +- **🧩 Pre-built Integrations:** Use readily available HTML/React chat widgets, search tools, Discord/Telegram bots, and more. +- **🔌 Flexible Deployment:** Works with major LLMs (OpenAI, Google, Anthropic) and local models (Ollama, llama_cpp). +- **🏢 Secure & Scalable:** Run privately and securely with Kubernetes support, designed for enterprise-grade reliability. + +**Contribute and Extend:** As an open-source project, community contributions are highly encouraged! If you develop valuable customizations or enhancements, consider contributing them back to the main repository to benefit other DocsGPT users. + ( diff --git a/docs/pages/quickstart.mdx b/docs/pages/quickstart.mdx new file mode 100644 index 00000000..cef1cd68 --- /dev/null +++ b/docs/pages/quickstart.mdx @@ -0,0 +1,84 @@ +--- +title: Quickstart - Launching DocsGPT Web App +description: Get started with DocsGPT quickly by launching the web application using the setup script. +--- + +# Quickstart + +**Prerequisites:** + +* **Docker:** Ensure you have Docker installed and running on your system. + +## Launching DocsGPT (macOS and Linux) + +The easiest way to launch DocsGPT is using the provided `setup.sh` script. This script automates the configuration process and offers several setup options. + +**Steps:** + +1. **Download the DocsGPT Repository:** + + First, you need to download the DocsGPT repository to your local machine. You can do this using Git: + + ```bash + git clone https://github.com/arc53/DocsGPT.git + cd DocsGPT + ``` + +2. **Run the `setup.sh` script:** + + Navigate to the DocsGPT directory in your terminal and execute the `setup.sh` script: + + ```bash + ./setup.sh + ``` + +3. **Follow the interactive setup:** + + The `setup.sh` script will guide you through an interactive menu with the following options: + + ``` + Welcome to DocsGPT Setup! + How would you like to proceed? + 1) Use DocsGPT Public API Endpoint (simple and free) + 2) Serve Local (with Ollama) + 3) Connect Local Inference Engine + 4) Connect Cloud API Provider + Choose option (1-4): + ``` + + Let's break down each option: + + * **1) Use DocsGPT Public API Endpoint (simple and free):** This is the simplest option to get started. It utilizes the DocsGPT public API, requiring no API keys or local model downloads. Choose this for a quick and easy setup. + + * **2) Serve Local (with Ollama):** This option allows you to run a Large Language Model locally using [Ollama](https://ollama.com/). You'll be prompted to choose between CPU or GPU for Ollama and select a model to download. This is a good option for local processing and experimentation. + + * **3) Connect Local Inference Engine:** If you are already running a local inference engine like Llama.cpp, Text Generation Inference (TGI), vLLM, or others, choose this option. You'll be asked to select your engine and provide the necessary connection details. This is for users with existing local LLM infrastructure. + + * **4) Connect Cloud API Provider:** This option lets you connect DocsGPT to a commercial Cloud API provider such as OpenAI, Google (Vertex AI/Gemini), Anthropic (Claude), Groq, HuggingFace Inference API, or Azure OpenAI. You will need an API key from your chosen provider. Select this if you prefer to use a powerful cloud-based LLM. + + After selecting an option and providing any required information (like API keys or model names), the script will configure your `.env` file and start DocsGPT using Docker Compose. + +4. **Access DocsGPT in your browser:** + + Once the setup is complete and Docker containers are running, navigate to [http://localhost:5173/](http://localhost:5173/) in your web browser to access the DocsGPT web application. + +5. **Stopping DocsGPT:** + + To stop DocsGPT, simply open a new terminal in the `DocsGPT` directory and run: + + ```bash + docker compose -f deployment/docker-compose.yaml down + ``` + (or the specific `docker compose` command shown at the end of the `setup.sh` execution, which may include optional compose files depending on your choices). + +## Launching DocsGPT (Windows) + +For Windows users, we recommend following the Docker deployment guide for detailed instructions. Please refer to the [Docker Deployment documentation](/Deploying/Docker-Deploying) for step-by-step instructions on setting up DocsGPT on Windows using Docker. + +**Important for Windows:** Ensure Docker Desktop is installed and running correctly on your Windows system before proceeding. + +## Advanced Configuration + +For more advanced customization of DocsGPT settings, such as configuring vector stores, embedding models, and other parameters, please refer to the [DocsGPT Settings documentation](/Deploying/DocsGPT-Settings). This guide explains how to modify the `.env` file or `settings.py` for deeper configuration. + +Enjoy using DocsGPT! \ No newline at end of file diff --git a/docs/public/civo.png b/docs/public/civo.png new file mode 100644 index 00000000..c786543f Binary files /dev/null and b/docs/public/civo.png differ diff --git a/docs/public/digitalocean.png b/docs/public/digitalocean.png new file mode 100644 index 00000000..2d90824a Binary files /dev/null and b/docs/public/digitalocean.png differ diff --git a/docs/public/kamatera.png b/docs/public/kamatera.png new file mode 100644 index 00000000..cf79872d Binary files /dev/null and b/docs/public/kamatera.png differ diff --git a/docs/public/lightsail.png b/docs/public/lightsail.png new file mode 100644 index 00000000..982ca63e Binary files /dev/null and b/docs/public/lightsail.png differ diff --git a/docs/public/railway.png b/docs/public/railway.png new file mode 100644 index 00000000..b63dbb83 Binary files /dev/null and b/docs/public/railway.png differ diff --git a/docs/theme.config.jsx b/docs/theme.config.jsx index 777a0ed5..77d8b9c1 100644 --- a/docs/theme.config.jsx +++ b/docs/theme.config.jsx @@ -49,7 +49,22 @@ const config = { light: 212, }, footer: { - text: `MIT ${new Date().getFullYear()} © DocsGPT`, + text: ( +
+ MIT {new Date().getFullYear()} © + + DocsGPT + + {' | '} + + GitHub + + {' | '} + + Blog + +
+ ), }, editLink: { content: 'Edit this page on GitHub', diff --git a/extensions/react-widget/README.md b/extensions/react-widget/README.md index b4159578..5b6222d2 100644 --- a/extensions/react-widget/README.md +++ b/extensions/react-widget/README.md @@ -13,7 +13,7 @@ npm install docsgpt ### React ```javascript - import { DocsGPTWidget } from "docsgpt"; + import { DocsGPTWidget } from "docsgpt-react"; const App = () => { return ; @@ -23,11 +23,11 @@ npm install docsgpt To link the widget to your api and your documents you can pass parameters to the component. ```javascript - import { DocsGPTWidget } from "docsgpt"; + import { DocsGPTWidget } from "docsgpt-react"; const App = () => { return ``` +# SearchBar + +The `SearchBar` component is an interactive search bar designed to provide search results based on **vector similarity search**. It also includes the capability to open the AI Chatbot, enabling users to query. + +--- + +### Importing the Component +```tsx +import { SearchBar } from "docsgpt-react"; +``` + +--- + +### Usage Example +```tsx + +``` + +--- + +## HTML embedding for Search bar + +```html + + + + + + SearchBar Embedding + + + + +
+ + + + + +``` + +### Props + +| **Prop** | **Type** | **Default Value** | **Description** | +|-----------------|-----------|-------------------------------------|--------------------------------------------------------------------------------------------------| +| **`apiKey`** | `string` | `"74039c6d-bff7-44ce-ae55-2973cbf13837"` | Your API key generated from the app. Used for authenticating requests. | +| **`apiHost`** | `string` | `"https://gptcloud.arc53.com"` | The base URL of the server hosting the vector similarity search and chatbot services. | +| **`theme`** | `"dark" \| "light"` | `"dark"` | The theme of the search bar. Accepts `"dark"` or `"light"`. | +| **`placeholder`** | `string` | `"Search or Ask AI..."` | Placeholder text displayed in the search input field. | +| **`width`** | `string` | `"256px"` | Width of the search bar. Accepts any valid CSS width value (e.g., `"300px"`, `"100%"`, `"20rem"`). | + + +Feel free to reach out if you need help customizing or extending the `SearchBar`! + ## Our github [DocsGPT](https://github.com/arc53/DocsGPT) diff --git a/extensions/react-widget/package-lock.json b/extensions/react-widget/package-lock.json index de4c228d..bae94d08 100644 --- a/extensions/react-widget/package-lock.json +++ b/extensions/react-widget/package-lock.json @@ -1,12 +1,12 @@ { "name": "docsgpt", - "version": "0.4.7", + "version": "0.4.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "docsgpt", - "version": "0.4.7", + "version": "0.4.9", "license": "Apache-2.0", "dependencies": { "@babel/plugin-transform-flow-strip-types": "^7.23.3", @@ -1885,6 +1885,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -2258,7 +2269,6 @@ "version": "2.12.0", "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.12.0.tgz", "integrity": "sha512-s+6pwEj+GfKf7vqGUzN9iSEPueUssCCQrCBUlcAfKrJe0a22hTUCjewpB0I7lNrCIULt8dkndD+sMdOrXsRl6Q==", - "dev": true, "dependencies": { "@mischnic/json-sourcemap": "^0.1.0", "@parcel/cache": "2.12.0", @@ -2298,7 +2308,6 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -2360,7 +2369,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.2.0.tgz", "integrity": "sha512-xlrmCPqy58D4Fg5umV7bpwDx5Vyt7MlnQPxW68vae5+BA4GSWetfZt+Cs5dtotMG2oCHzZxhIPt7YZ7NRyQzLA==", - "dev": true, "dependencies": { "nullthrows": "^1.1.1" }, @@ -4560,7 +4568,7 @@ "version": "0.5.11", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", - "dev": true, + "devOptional": true, "dependencies": { "tslib": "^2.4.0" } @@ -4590,6 +4598,35 @@ "@types/trusted-types": "*" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "peer": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4621,6 +4658,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "peer": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -4662,11 +4709,198 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "peer": true + }, "node_modules/abortcontroller-polyfill": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz", - "integrity": "sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==", - "dev": true + "integrity": "sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, "node_modules/ajv": { "version": "6.12.6", @@ -4771,7 +5005,6 @@ "version": "3.0.9", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", - "dev": true, "dependencies": { "safe-buffer": "^5.0.1" } @@ -4802,9 +5035,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "funding": [ { "type": "opencollective", @@ -4820,10 +5053,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -4832,6 +5065,13 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "peer": true + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4860,9 +5100,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001625", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz", - "integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==", + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", "funding": [ { "type": "opencollective", @@ -4876,7 +5116,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", @@ -4922,7 +5163,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, "engines": { "node": ">=0.8" } @@ -5132,7 +5372,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-7.0.0.tgz", "integrity": "sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==", - "dev": true, "engines": { "node": ">=6" } @@ -5140,13 +5379,12 @@ "node_modules/dotenv-expand": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "dev": true + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" }, "node_modules/electron-to-chromium": { - "version": "1.4.788", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.788.tgz", - "integrity": "sha512-ubp5+Ev/VV8KuRoWnfP2QF2Bg+O2ZFdb49DiiNbz2VmgkIqrnyYaqIOqj8A6K/3p1xV0QcU5hBQ1+BmB6ot1OA==" + "version": "1.5.72", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.72.tgz", + "integrity": "sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw==" }, "node_modules/emojis-list": { "version": "3.0.0", @@ -5157,6 +5395,20 @@ "node": ">= 4" } }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -5185,10 +5437,17 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "peer": true + }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "engines": { "node": ">=6" } @@ -5201,6 +5460,53 @@ "node": ">=0.8.0" } }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5210,6 +5516,16 @@ "node": ">=0.10.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5300,6 +5616,13 @@ "node": ">=6" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "peer": true + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -5308,6 +5631,13 @@ "node": ">=4" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "peer": true + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -5513,6 +5843,47 @@ "node": ">=0.12.0" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5813,6 +6184,16 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, "node_modules/loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", @@ -5902,6 +6283,13 @@ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "license": "MIT" }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "peer": true + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -5914,6 +6302,29 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5960,9 +6371,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -5976,6 +6387,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "peer": true + }, "node_modules/node-addon-api": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", @@ -6006,9 +6424,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" }, "node_modules/npm": { "version": "10.8.1", @@ -8615,9 +9033,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -8750,6 +9168,16 @@ "node": ">=6" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -8888,7 +9316,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -8938,6 +9365,16 @@ "semver": "bin/semver.js" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -8959,6 +9396,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -9025,6 +9473,16 @@ "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/term-size": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", @@ -9037,6 +9495,86 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/terser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "peer": true + }, "node_modules/timsort": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", @@ -9083,7 +9621,6 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9098,6 +9635,13 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "peer": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -9139,9 +9683,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "funding": [ { "type": "opencollective", @@ -9157,8 +9701,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -9184,11 +9728,101 @@ "node": ">= 4" } }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/weak-lru-cache": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==" }, + "node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/extensions/react-widget/package.json b/extensions/react-widget/package.json index 90db38aa..75239685 100644 --- a/extensions/react-widget/package.json +++ b/extensions/react-widget/package.json @@ -1,6 +1,6 @@ { "name": "docsgpt", - "version": "0.4.7", + "version": "0.4.9", "private": false, "description": "DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖.", "source": "./src/index.html", @@ -30,9 +30,10 @@ "styled-components": "^5" }, "scripts": { - "build": "parcel build src/main.tsx --public-url ./", + "build": "parcel build src/browser.tsx --public-url ./", "build:react": "parcel build src/index.ts", - "dev": "parcel src/index.html -p 3000", + "serve": "parcel serve -p 3000", + "dev": "parcel -p 3000", "test": "jest", "lint": "eslint", "check": "tsc --noEmit", diff --git a/extensions/react-widget/publish.sh b/extensions/react-widget/publish.sh index c4545d85..129c4bcf 100755 --- a/extensions/react-widget/publish.sh +++ b/extensions/react-widget/publish.sh @@ -1,43 +1,85 @@ #!/bin/bash -## chmod +x publish.sh - to upgrade ownership set -e -cat package.json >> package_copy.json -cat package-lock.json >> package-lock_copy.json + +# Create backup of original files +cp package.json package_original.json +cp package-lock.json package-lock_original.json + +# Store the latest version after publishing +LATEST_VERSION="" + publish_package() { - PACKAGE_NAME=$1 - BUILD_COMMAND=$2 - # Update package name in package.json - jq --arg name "$PACKAGE_NAME" '.name=$name' package.json > temp.json && mv temp.json package.json + PACKAGE_NAME=$1 + BUILD_COMMAND=$2 + IS_REACT=$3 - # Remove 'target' key if the package name is 'docsgpt-react' - if [ "$PACKAGE_NAME" = "docsgpt-react" ]; then - jq 'del(.targets)' package.json > temp.json && mv temp.json package.json - fi + echo "Preparing to publish ${PACKAGE_NAME}..." + + # Restore original package.json state before each publish + cp package_original.json package.json + cp package-lock_original.json package-lock.json - if [ -d "dist" ]; then - echo "Deleting existing dist directory..." - rm -rf dist - fi + # Update package name in package.json + jq --arg name "$PACKAGE_NAME" '.name=$name' package.json > temp.json && mv temp.json package.json - npm version patch + # Handle targets based on package type + if [ "$IS_REACT" = "true" ]; then + echo "Removing targets for React library build..." + jq 'del(.targets)' package.json > temp.json && mv temp.json package.json + fi - npm run "$BUILD_COMMAND" + # Clean dist directory + if [ -d "dist" ]; then + echo "Cleaning dist directory..." + rm -rf dist + fi - # Publish to npm - npm publish - # Clean up - mv package_copy.json package.json - mv package-lock_copy.json package-lock.json - echo "Published ${PACKAGE_NAME}" + # update version and store it + LATEST_VERSION=$(npm version patch) + echo "New version: ${LATEST_VERSION}" + + # Build package + npm run "$BUILD_COMMAND" + + # Replace npm publish with npm pack for testing + npm publish + + echo "Successfully packaged ${PACKAGE_NAME}" + + # Log the bundle size + TARBALL="${PACKAGE_NAME}-${LATEST_VERSION#v}.tgz" + if [ -f "$TARBALL" ]; then + BUNDLE_SIZE=$(du -h "$TARBALL" | cut -f1) + echo "Bundle size for ${PACKAGE_NAME}: ${BUNDLE_SIZE}" + else + echo "Error: ${TARBALL} not found." + exit 1 + fi } -# Publish docsgpt package -publish_package "docsgpt" "build" +# First publish docsgpt (HTML bundle) +publish_package "docsgpt" "build" "false" -# Publish docsgpt-react package -publish_package "docsgpt-react" "build:react" +# Then publish docsgpt-react (React library) +publish_package "docsgpt-react" "build:react" "true" +# Restore original state but keep the updated version +cp package_original.json package.json +cp package-lock_original.json package-lock.json -rm -rf package_copy.json -rm -rf package-lock_copy.json -echo "---Process completed---" \ No newline at end of file +# Update the version in the final package.json +jq --arg version "${LATEST_VERSION#v}" '.version=$version' package.json > temp.json && mv temp.json package.json + +# Run npm install to update package-lock.json with the new version +npm install --package-lock-only + +# Cleanup backup files +rm -f package_original.json +rm -f package-lock_original.json +rm -f temp.json + +echo "---Process completed---" +echo "Final version in package.json: $(jq -r '.version' package.json)" +echo "Final version in package-lock.json: $(jq -r '.version' package-lock.json)" +echo "Generated test packages:" +ls *.tgz diff --git a/extensions/react-widget/src/App.tsx b/extensions/react-widget/src/App.tsx index ec9de47b..4bb24bae 100644 --- a/extensions/react-widget/src/App.tsx +++ b/extensions/react-widget/src/App.tsx @@ -1,11 +1,11 @@ import React from "react" import {DocsGPTWidget} from "./components/DocsGPTWidget" -const App = () => { +import {SearchBar} from "./components/SearchBar" +export const App = () => { return (
+
) -} - -export default App \ No newline at end of file +} \ No newline at end of file diff --git a/extensions/react-widget/src/browser.tsx b/extensions/react-widget/src/browser.tsx new file mode 100644 index 00000000..8bee7748 --- /dev/null +++ b/extensions/react-widget/src/browser.tsx @@ -0,0 +1,22 @@ +//exports browser ready methods + +import { createRoot } from "react-dom/client"; + +import { DocsGPTWidget } from './components/DocsGPTWidget'; +import { SearchBar } from './components/SearchBar'; +import React from "react"; +if (typeof window !== 'undefined') { + const renderWidget = (elementId: string, props = {}) => { + const root = createRoot(document.getElementById(elementId) as HTMLElement); + root.render(); + }; + const renderSearchBar = (elementId: string, props = {}) => { + const root = createRoot(document.getElementById(elementId) as HTMLElement); + root.render(); + }; + (window as any).renderDocsGPTWidget = renderWidget; + + (window as any).renderSearchBar = renderSearchBar; +} + +export { DocsGPTWidget, SearchBar }; diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 6ddec1c0..d6273eaa 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -1,11 +1,11 @@ "use client"; import React, { useRef } from 'react' import DOMPurify from 'dompurify'; -import styled, { keyframes, ThemeProvider, } from 'styled-components'; -import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon, } from '@radix-ui/react-icons'; -import { FEEDBACK, MESSAGE_TYPE, Query, Status, WidgetProps } from '../types/index'; +import styled, { keyframes, css } from 'styled-components'; +import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon } from '@radix-ui/react-icons'; +import { FEEDBACK, MESSAGE_TYPE, Query, Status, WidgetCoreProps, WidgetProps } from '../types/index'; import { fetchAnswerStreaming, sendFeedback } from '../requests/streamingApi'; -import QuerySources from "./QuerySources"; +import { ThemeProvider } from 'styled-components'; import Like from "../assets/like.svg" import Dislike from "../assets/dislike.svg" import MarkdownIt from 'markdown-it'; @@ -23,7 +23,6 @@ const themes = { bg: "#38383b" } }, - light: { bg: '#fff', text: '#000', @@ -50,8 +49,86 @@ const sizesConfig = { maxHeight: custom.maxHeight || '70vh', }), }; +const createBox = keyframes` + 0% { + transform: scale(0.6); + } + 90% { + transform: scale(1.02); + } + 100% { + transform: scale(1); + } +` +const closeBox = keyframes` + 0% { + transform: scale(1); + } + 10% { + transform: scale(1.02); + } + 100% { + transform: scale(0.6); + } +` +const openContainer = keyframes` + 0% { + width: 200px; + height: 100px; + } + 100% { + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; + border-radius: 12px; + }` +const closeContainer = keyframes` + 0% { + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; + border-radius: 12px; + } + 100% { + width: 200px; + height: 100px; + } +` +const fadeIn = keyframes` + from { + opacity: 0; + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; + } +` +const fadeOut = keyframes` + from { + opacity: 1; + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; + } + to { + opacity: 0; + transform: scale(0.9); + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; + } +` +const scaleAnimation = keyframes` + from { + transform: scale(1.2); + } + to { + transform: scale(1); + } +` const Overlay = styled.div` position: fixed; top: 0; @@ -62,53 +139,35 @@ const Overlay = styled.div` z-index: 999; transition: opacity 0.5s; ` + + const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` all: initial; position: fixed; right: ${props => props.modal ? '50%' : '10px'}; bottom: ${props => props.modal ? '50%' : '10px'}; - z-index: 1000; - display: none; + z-index: 1001; transform-origin:100% 100%; + display: block; + &.modal{ + transform : translate(50%,50%); + } &.open { - animation: createBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; + animation: css ${createBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; } &.close { - animation: closeBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; + animation: css ${closeBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; } - ${props => props.modal && - "transform : translate(50%,50%);" - } align-items: center; text-align: left; - @keyframes createBox { - 0% { - transform: scale(0.6); - } - 90% { - transform: scale(1.02); - } - 100% { - transform: scale(1); - } - } - - @keyframes closeBox { - 0% { - transform: scale(1); - } - 10% { - transform: scale(1.02); - } - 100% { - transform: scale(0.6); - } - } `; + const StyledContainer = styled.div<{ isOpen: boolean }>` all: initial; max-height: ${(props) => props.theme.dimensions.maxHeight}; max-width: ${(props) => props.theme.dimensions.maxWidth}; + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height} ; position: relative; flex-direction: column; justify-content: space-between; @@ -121,68 +180,20 @@ const StyledContainer = styled.div<{ isOpen: boolean }>` box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.1); padding: 26px 26px 0px 26px; animation: ${({ isOpen, theme }) => - theme.dimensions.size === 'large' - ? isOpen - ? 'fadeIn 150ms ease-in forwards' - : 'fadeOut 150ms ease-in forwards' - : isOpen - ? 'openContainer 150ms ease-in forwards' - : 'closeContainer 250ms ease-in forwards'}; - @keyframes openContainer { - 0% { - width: 200px; - height: 100px; - } - 100% { - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; - border-radius: 12px; - } - } - @keyframes closeContainer { - 0% { - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; - border-radius: 12px; - } - 100% { - width: 200px; - height: 100px; - } - } - @keyframes fadeIn { - from { - opacity: 0; - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; - transform: scale(0.9); - } - to { - opacity: 1; - transform: scale(1); - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; - } - } - @keyframes fadeOut { - from { - opacity: 1; - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; - } - to { - opacity: 0; - transform: scale(0.9); - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; - } - } + theme.dimensions.size === 'large' + ? isOpen + ? css`${fadeIn} 150ms ease-in forwards` + : css` ${fadeOut} 150ms ease-in forwards` + : isOpen + ? css`${openContainer} 150ms ease-in forwards` + : css`${closeContainer} 250ms ease-in forwards`}; @media only screen and (max-width: 768px) { max-height: 100vh; max-width: 80vw; overflow: auto; } `; + const FloatingButton = styled.div<{ bgcolor: string, hidden: boolean, isAnimatingButton: boolean }>` position: fixed; display: ${props => props.hidden ? "none" : "flex"}; @@ -200,7 +211,7 @@ const FloatingButton = styled.div<{ bgcolor: string, hidden: boolean, isAnimatin background: ${props => props.bgcolor}; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); cursor: pointer; - animation: ${props => props.isAnimatingButton ? 'scaleAnimation 200ms forwards' : 'none'}; + animation: ${props => props.isAnimatingButton ? css`${scaleAnimation} 200ms forwards` : 'none'}; &:hover { transform: scale(1.1); transition: transform 0.2s ease-in-out; @@ -208,17 +219,7 @@ const FloatingButton = styled.div<{ bgcolor: string, hidden: boolean, isAnimatin &:not(:hover) { transition: transform 0.2s ease-in-out; } - - @keyframes scaleAnimation { - from { - transform: scale(1.2); - } - to { - transform: scale(1); - } - } `; - const CancelButton = styled.button` cursor: pointer; position: absolute; @@ -275,7 +276,6 @@ const Conversation = styled.div` scrollbar-width: thin; scrollbar-color: #4a4a4a transparent; /* thumb color track color */ `; - const Feedback = styled.div` background-color: transparent; font-weight: normal; @@ -284,7 +284,6 @@ const Feedback = styled.div` padding: 6px; clear: both; `; - const MessageBubble = styled.div<{ type: MESSAGE_TYPE }>` display: block; font-size: 16px; @@ -296,7 +295,6 @@ const MessageBubble = styled.div<{ type: MESSAGE_TYPE }>` visibility: visible ; } `; - const Message = styled.div<{ type: MESSAGE_TYPE }>` background: ${props => props.type === 'QUESTION' ? 'linear-gradient(to bottom right, #8860DB, #6D42C5)' : @@ -371,7 +369,6 @@ const ErrorAlert = styled.div` border-radius: 6px; justify-content: space-evenly; ` - //dot loading animation const dotBounce = keyframes` 0%, 80%, 100% { @@ -386,7 +383,6 @@ const DotAnimation = styled.div` display: inline-block; animation: ${dotBounce} 1s infinite ease-in-out; `; - // delay classes as styled components const Delay = styled(DotAnimation) <{ delay: number }>` animation-delay: ${props => props.delay + 'ms'}; @@ -397,7 +393,6 @@ const PromptContainer = styled.form` display: flex; justify-content: space-evenly; `; - const StyledInput = styled.input` width: 100%; border: 1px solid #686877; @@ -429,7 +424,6 @@ const StyledButton = styled.button` &:disabled { background-image: linear-gradient(to bottom right, #2d938f, #b31877); }`; - const HeroContainer = styled.div` position: relative; width: 90%; @@ -439,7 +433,6 @@ const HeroContainer = styled.div` margin: 16px auto; padding: 2px; `; - const HeroWrapper = styled.div` display: flex; flex-direction: column; @@ -457,7 +450,6 @@ const HeroTitle = styled.h3` margin:0px ; padding: 0px; `; - const HeroDescription = styled.p` color: ${props => props.theme.text}; font-size: 12px; @@ -490,12 +482,49 @@ const Hero = ({ title, description, theme }: { title: string, description: strin ); }; +export const DocsGPTWidget = (props: WidgetProps) => { + const { + buttonIcon = 'https://d3dg1063dc54p9.cloudfront.net/widget/chat.svg', + buttonText = 'Ask a question', + buttonBg = 'linear-gradient(to bottom right, #5AF0EC, #E80D9D)', + defaultOpen = false, + ...coreProps + } = props + const [open, setOpen] = React.useState(defaultOpen); + const [isAnimatingButton, setIsAnimatingButton] = React.useState(false); + const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); -export const DocsGPTWidget = ({ + React.useEffect(() => { + if (isFloatingButtonVisible) + setTimeout(() => setIsAnimatingButton(true), 250); + return () => { + setIsAnimatingButton(false) + } + }, [isFloatingButtonVisible]) + + const handleClose = () => { + setIsFloatingButtonVisible(true); + setOpen(false); + }; + const handleOpen = () => { + setOpen(true); + setIsFloatingButtonVisible(false); + } + return ( + <> + + + + ) +} +export const WidgetCore = ({ apiHost = 'https://gptcloud.arc53.com', - apiKey = '0d7407f7-a843-42fb-ad83-dd4a213a935d', + apiKey = '82962c9a-aa77-4152-94e5-a4f84fd44c6a', avatar = 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png', title = 'Get AI assistance', description = 'DocsGPT\'s AI Chatbot is here to help', @@ -503,30 +532,40 @@ export const DocsGPTWidget = ({ heroDescription = 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.', size = 'small', theme = 'dark', - buttonIcon = 'https://d3dg1063dc54p9.cloudfront.net/widget/chat.svg', - buttonText = 'Ask a question', - buttonBg = 'linear-gradient(to bottom right, #5AF0EC, #E80D9D)', collectFeedback = true, - showSources = true, - deafultOpen = false -}: WidgetProps) => { - const [prompt, setPrompt] = React.useState(''); + isOpen = false, + prefilledQuery = "", + handleClose +}: WidgetCoreProps) => { + const [prompt, setPrompt] = React.useState(""); + const [mounted, setMounted] = React.useState(false); const [status, setStatus] = React.useState('idle'); - const [queries, setQueries] = React.useState([]) - const [conversationId, setConversationId] = React.useState(null) - const [open, setOpen] = React.useState(deafultOpen) + const [queries, setQueries] = React.useState([]); + const [conversationId, setConversationId] = React.useState(null); const [eventInterrupt, setEventInterrupt] = React.useState(false); //click or scroll by user while autoScrolling - const [isAnimatingButton, setIsAnimatingButton] = React.useState(false); - const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); - const isBubbleHovered = useRef(false) - const widgetRef = useRef(null) + + const isBubbleHovered = useRef(false); const endMessageRef = React.useRef(null); const md = new MarkdownIt(); + React.useEffect(() => { + if (isOpen) { + setMounted(true); // Mount the component + appendQuery(prefilledQuery) + } else { + // Wait for animations before unmounting + const timeout = setTimeout(() => { + setMounted(false) + }, 250); + return () => clearTimeout(timeout); + } + }, [isOpen]); + + + const handleUserInterrupt = () => { (status === 'loading') && setEventInterrupt(true); } - const scrollToBottom = (element: Element | null) => { //recursive function to scroll to the last child of the last child ... // to get to the bottom most element @@ -540,7 +579,6 @@ export const DocsGPTWidget = ({ const lastChild = element?.children?.[element.children.length - 1] lastChild && scrollToBottom(lastChild) }; - React.useEffect(() => { !eventInterrupt && scrollToBottom(endMessageRef.current); }, [queries.length, queries[queries.length - 1]?.response]); @@ -580,14 +618,14 @@ export const DocsGPTWidget = ({ try { await fetchAnswerStreaming( { - question, - apiKey, - apiHost, + question: question, + apiKey: apiKey, + apiHost: apiHost, history: queries, - conversationId, + conversationId: conversationId, onEvent: (event: MessageEvent) => { const data = JSON.parse(event.data); - + // check if the 'end' event has been received if (data.type === 'end') { setStatus('idle'); } @@ -600,13 +638,8 @@ export const DocsGPTWidget = ({ setQueries(updatedQueries); setStatus('idle') } - else if (data.type === 'source') { - const updatedQueries = [...queries]; - updatedQueries[updatedQueries.length - 1].sources = data.source; - setQueries(updatedQueries); - console.log("SOURCE:", data); - - + else if (data.type === 'source') { + // handle the case where data type === 'source' } else { const result = data.answer ? data.answer : ''; //Fallback to an empty string if data.answer is undefined @@ -623,154 +656,144 @@ export const DocsGPTWidget = ({ updatedQueries[updatedQueries.length - 1].error = 'Something went wrong !' setQueries(updatedQueries); setStatus('idle') + //setEventInterrupt(false) } + + } + // submit handler + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await appendQuery(prompt) } - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() + const appendQuery = async (userQuery:string) => { + console.log(userQuery) + if(!userQuery) + return; + setEventInterrupt(false); - queries.push({ prompt }) - setPrompt('') - await stream(prompt) + queries.push({ prompt:userQuery}); + setPrompt(''); + await stream(userQuery); } - const handleImageError = (event: React.SyntheticEvent) => { event.currentTarget.src = "https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"; }; - const handleClose = () => { - setOpen(false); - setTimeout(() => { - if (widgetRef.current) widgetRef.current.style.display = "none"; - setIsFloatingButtonVisible(true); - setIsAnimatingButton(true); - setTimeout(() => setIsAnimatingButton(false), 200); - }, 250) - }; - const handleOpen = () => { - setOpen(true); - setIsFloatingButtonVisible(false); - if (widgetRef.current) - widgetRef.current.style.display = 'block' - } + const dimensions = typeof size === 'object' && 'custom' in size ? sizesConfig.getCustom(size.custom) : sizesConfig[size]; - + if (!mounted) return null; return ( - {open && size === 'large' && + {isOpen && size === 'large' && } - - - { -
- - - -
- docs-gpt - - {title} - {description} - -
-
- - { - queries.length > 0 ? queries?.map((query, index) => { - return ( - - { - query.prompt && - - {query.prompt} - - - } - { - query.response ? { isBubbleHovered.current = true }} type='ANSWER'> - {showSources && query.sources && ( - - )} - - - + {( + + +
+ + + +
+ docs-gpt + + {title} + {description} + +
+
+ + { + queries.length > 0 ? queries?.map((query, index) => { + return ( + + { + query.prompt && + + {query.prompt} + + + } + { + query.response ? { isBubbleHovered.current = true }} type='ANSWER'> + + + - {collectFeedback && - - handleFeedback("LIKE", index)} /> - handleFeedback("DISLIKE", index)} /> - } - - : (
- { - query.error ? + {collectFeedback && + + handleFeedback("LIKE", index)} /> + handleFeedback("DISLIKE", index)} /> + } + + :
+ { + query.error ? - -
-
Network Error
- {query.error} -
-
- : - - . - . - . - - - } -
- ) - } - - ); - }) - : - } - -
- - setPrompt(event.target.value)} - type='text' placeholder="Ask your question" /> - - - - - - Powered by  - DocsGPT - -
- } - + +
+
Network Error
+ {query.error} +
+
+ : + + . + . + . + + + } +
+ } +
) + }) + : + } +
+
+ + setPrompt(event.target.value)} + type='text' placeholder="Ask your question" /> + + + + + + Powered by  + DocsGPT + +
+
+
+ ) + }
) } \ No newline at end of file diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx new file mode 100644 index 00000000..c647991f --- /dev/null +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -0,0 +1,572 @@ +import React from 'react'; +import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; +import { WidgetCore } from './DocsGPTWidget'; +import { SearchBarProps } from '@/types'; +import { getSearchResults } from '../requests/searchAPI'; +import { Result } from '@/types'; +import MarkdownIt from 'markdown-it'; +import { getOS, processMarkdownString } from '../utils/helper'; +import DOMPurify from 'dompurify'; +import { + CodeIcon, + TextAlignLeftIcon, + HeadingIcon, + ReaderIcon, + ListBulletIcon, + QuoteIcon +} from '@radix-ui/react-icons'; +const themes = { + dark: { + bg: '#202124', + text: '#EDEDED', + primary: { + text: "#FAFAFA", + bg: '#111111' + }, + secondary: { + text: "#A1A1AA", + bg: "#38383b" + } + }, + light: { + bg: '#EAEAEA', + text: '#171717', + primary: { + text: "#222327", + bg: "#fff" + }, + secondary: { + text: "#A1A1AA", + bg: "#F6F6F6" + } + } +} + +const GlobalStyle = createGlobalStyle` + .highlight { + color:#007EE6; + } +`; + +const loadGeistFont = () => { + const link = document.createElement('link'); + link.href = 'https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap'; + link.rel = 'stylesheet'; + document.head.appendChild(link); +}; + +const Main = styled.div` + all: initial; + font-family: 'Geist', sans-serif; +` +const SearchButton = styled.button<{ inputWidth: string }>` + padding: 6px 6px; + font-family: inherit; + width: ${({ inputWidth }) => inputWidth}; + border-radius: 8px; + display: inline; + color: ${props => props.theme.secondary.text}; + outline: none; + border: none; + background-color: ${props => props.theme.secondary.bg}; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + transition: background-color 128ms linear; + text-align: left; + cursor: pointer; +` + +const Container = styled.div` + position: relative; + display: inline-block; +` +const SearchResults = styled.div` + position: fixed; + display: flex; + flex-direction: column; + background-color: ${props => props.theme.primary.bg}; + border: 1px solid ${props => props.theme.bg}; + border-radius: 15px; + padding: 8px 0px 8px 0px; + width: 792px; + max-width: 90vw; + height: 396px; + z-index: 100; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + color: ${props => props.theme.primary.text}; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(16px); + box-sizing: border-box; + + @media only screen and (max-width: 768px) { + height: 80vh; + width: 90vw; + } +`; + +const SearchResultsScroll = styled.div` + flex: 1; + overflow-y: auto; + overflow-x: hidden; + scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: #383838 transparent; + padding: 0 16px; +`; + +const IconTitleWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; + + .element-icon{ + margin: 4px; + } +`; + +const Title = styled.h3` + font-size: 15px; + font-weight: 400; + color: ${props => props.theme.primary.text}; + margin: 0; + overflow-wrap: break-word; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; +`; +const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; +const Content = styled.div` + display: flex; + margin-left: 8px; + flex-direction: column; + gap: 8px; + padding: 4px 0px 0px 12px; + font-size: 15px; + color: ${props => props.theme.primary.text}; + line-height: 1.6; + border-left: 2px solid #585858; + overflow: hidden; +` +const ContentSegment = styled.div` + display: flex; + align-items: flex-start; + gap: 8px; + padding-right: 16px; + overflow-wrap: break-word; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; +` + +const ResultWrapper = styled.div` + display: flex; + align-items: flex-start; + width: 100%; + box-sizing: border-box; + padding: 8px 16px; + cursor: pointer; + background-color: ${props => props.theme.primary.bg}; + font-family: 'Geist', sans-serif; + transition: background-color 0.2s; + border-radius: 8px; + + word-wrap: break-word; + overflow-wrap: break-word; + word-break: break-word; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + background-color: ${props => props.theme.bg}; + } +` +const Markdown = styled.div` +line-height:18px; +font-size: 11px; +white-space: pre-wrap; + pre { + padding: 8px; + width: 90%; + font-size: 11px; + border-radius: 6px; + overflow-x: auto; + background-color: #1B1C1F; + color: #fff ; + } + + h1,h2 { + font-size: 14px; + font-weight: 600; + color: ${(props) => props.theme.text}; + opacity: 0.8; + } + + + h3 { + font-size: 12px; + } + + p { + margin: 0px; + line-height: 1.35rem; + font-size: 11px; + } + + code:not(pre code) { + border-radius: 6px; + padding: 2px 2px; + margin: 2px; + font-size: 9px; + display: inline; + background-color: #646464; + color: #fff ; + } + img{ + max-width: 50%; + } + code { + overflow-x: auto; + } + a{ + color: #007ee6; + } +` +const Toolkit = styled.kbd` + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + background-color: ${(props) => props.theme.primary.bg}; + color: ${(props) => props.theme.secondary.text}; + font-weight: 600; + font-size: 10px; + padding: 3px 6px; + border: 1px solid ${(props) => props.theme.secondary.text}; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + pointer-events: none; +` +const Loader = styled.div` + margin: 2rem auto; + border: 4px solid ${props => props.theme.secondary.text}; + border-top: 4px solid ${props => props.theme.primary.bg}; + border-radius: 50%; + width: 12px; + height: 12px; + animation: spin 1s linear infinite; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +`; + +const NoResults = styled.div` + margin-top: 2rem; + text-align: center; + font-size: 14px; + color: #888; +`; +const AskAIButton = styled.button` + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + width: calc(100% - 32px); + margin: 0 16px 16px 16px; + box-sizing: border-box; + height: 50px; + padding: 8px 24px; + border: none; + border-radius: 6px; + background-color: ${props => props.theme.bg}; + color: ${props => props.theme.text}; + cursor: pointer; + transition: background-color 0.2s, box-shadow 0.2s; + font-size: 16px; + + &:hover { + opacity: 0.8; + } +` +const SearchHeader = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid ${props => props.theme.bg}; +` + +const TextField = styled.input` + width: calc(100% - 32px); + margin: 0 16px; + padding: 12px 16px; + border: none; + background-color: transparent; + color: ${props => props.theme.text}; + font-size: 20px; + font-weight: 400; + outline: none; + + &:focus { + border-color: none; + } +` + +const EscapeInstruction = styled.kbd` + display: flex; + align-items: center; + justify-content: center; + margin: 12px 16px 0; + padding: 4px 8px; + border-radius: 4px; + background-color: transparent; + border: 1px solid ${props => props.theme.secondary.text}; + color: ${props => props.theme.text}; + font-size: 12px; + font-family: 'Geist', sans-serif; + white-space: nowrap; + cursor: pointer; + width: fit-content; + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } +` +export const SearchBar = ({ + apiKey = "74039c6d-bff7-44ce-ae55-2973cbf13837", + apiHost = "https://gptcloud.arc53.com", + theme = "dark", + placeholder = "Search or Ask AI...", + width = "256px", + buttonText = "Search here" +}: SearchBarProps) => { + const [input, setInput] = React.useState(""); + const [loading, setLoading] = React.useState(false); + const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); + const inputRef = React.useRef(null); + const containerRef = React.useRef(null); + const [isResultVisible, setIsResultVisible] = React.useState(false); + const [results, setResults] = React.useState([]); + const debounceTimeout = React.useRef | null>(null); + const abortControllerRef = React.useRef(null); + const browserOS = getOS(); + const isTouch = 'ontouchstart' in window; + + const getKeyboardInstruction = () => { + if (isResultVisible) return "Enter"; + return browserOS === 'mac' ? '⌘ + K' : 'Ctrl + K'; + }; + + React.useEffect(() => { + loadGeistFont() + const handleClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setIsResultVisible(false); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if ( + ((browserOS === 'win' || browserOS === 'linux') && event.ctrlKey && event.key === 'k') || + (browserOS === 'mac' && event.metaKey && event.key === 'k') + ) { + event.preventDefault(); + inputRef.current?.focus(); + setIsResultVisible(true); + } else if (event.key === 'Escape') { + setIsResultVisible(false); + } + }; + + + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleKeyDown); + }; + }, []); + + React.useEffect(() => { + if (!input) { + setResults([]); + return; + } + setLoading(true); + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + debounceTimeout.current = setTimeout(() => { + getSearchResults(input, apiKey, apiHost, abortController.signal) + .then((data) => setResults(data)) + .catch((err) => !abortController.signal.aborted && console.log(err)) + .finally(() => setLoading(false)); + }, 500); + + return () => { + abortController.abort(); + clearTimeout(debounceTimeout.current ?? undefined); + }; + }, [input]) + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + openWidget(); + } + }; + + const openWidget = () => { + setIsWidgetOpen(true); + setIsResultVisible(false); + }; + + const handleClose = () => { + setIsWidgetOpen(false); + setIsResultVisible(true); + }; + + return ( + +
+ + + setIsResultVisible(true)} + inputWidth={width} + > + {buttonText} + + { + isResultVisible && ( + + + setInput(e.target.value)} + onKeyDown={(e) => handleKeyDown(e)} + placeholder={placeholder} + autoFocus + /> + setIsResultVisible(false)}> + Esc + + + + DocsGPT + Ask the AI + + + {!loading ? ( + results.length > 0 ? ( + results.map((res, key) => { + const containsSource = res.source !== 'local'; + const processedResults = processMarkdownString(res.text, input); + if (processedResults) + return ( + { + if (!containsSource) return; + window.open(res.source, '_blank', 'noopener, noreferrer'); + }} + > +
+ + + + {res.title} + + + {processedResults.map((element, index) => ( + + + {element.tag === 'code' && } + {(element.tag === 'bulletList' || element.tag === 'numberedList') && } + {element.tag === 'text' && } + {element.tag === 'heading' && } + {element.tag === 'blockquote' && } + +
+ + ))} + + +
+ + ); + return null; + }) + ) : ( + No results found + ) + ) : ( + + )} + + + ) + } + { + isTouch ? + + { + setIsWidgetOpen(true) + }} + title={"Tap to Ask the AI"}> + Tap + + : + + {getKeyboardInstruction()} + + } + + +
+
+ ) +} diff --git a/extensions/react-widget/src/index.html b/extensions/react-widget/src/index.html index 0f0710d5..40eaad15 100644 --- a/extensions/react-widget/src/index.html +++ b/extensions/react-widget/src/index.html @@ -9,11 +9,11 @@
- - --> diff --git a/extensions/react-widget/src/index.ts b/extensions/react-widget/src/index.ts index 1efa89a6..5f2e30e8 100644 --- a/extensions/react-widget/src/index.ts +++ b/extensions/react-widget/src/index.ts @@ -1 +1,3 @@ -export { DocsGPTWidget } from "./components/DocsGPTWidget"; \ No newline at end of file +//exports methods for React +export {SearchBar} from "./components/SearchBar" +export { DocsGPTWidget } from "./components/DocsGPTWidget"; diff --git a/extensions/react-widget/src/main.tsx b/extensions/react-widget/src/main.tsx index 368dc394..f238c51e 100644 --- a/extensions/react-widget/src/main.tsx +++ b/extensions/react-widget/src/main.tsx @@ -1,14 +1,8 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { DocsGPTWidget } from './components/DocsGPTWidget'; -if (typeof window !== 'undefined') { - const renderWidget = (elementId: string, props={}) => { - const root = createRoot(document.getElementById(elementId) as HTMLElement); - root.render( - - ); - }; - (window as any).renderDocsGPTWidget = renderWidget; -} -export { DocsGPTWidget }; \ No newline at end of file +//development +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import React from "react"; +const container = document.getElementById("app") as HTMLElement; +const root = createRoot(container) +root.render(); diff --git a/extensions/react-widget/src/requests/searchAPI.ts b/extensions/react-widget/src/requests/searchAPI.ts new file mode 100644 index 00000000..7411a810 --- /dev/null +++ b/extensions/react-widget/src/requests/searchAPI.ts @@ -0,0 +1,37 @@ +import { Result } from "@/types"; + +async function getSearchResults(question: string, apiKey: string, apiHost: string, signal: AbortSignal): Promise { + + const payload = { + question, + api_key: apiKey + }; + + try { + const response = await fetch(`${apiHost}/api/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + signal: signal + }); + + if (!response.ok) { + throw new Error(`Error: ${response.status}`); + } + + const data: Result[] = await response.json(); + return data; + + } catch (error) { + if (!(error instanceof DOMException && error.name == "AbortError")) { + console.error("Failed to fetch documents:", error); + } + throw error; + } +} + +export { + getSearchResults +} \ No newline at end of file diff --git a/extensions/react-widget/src/types/index.ts b/extensions/react-widget/src/types/index.ts index 6a6d30a7..34ab66a2 100644 --- a/extensions/react-widget/src/types/index.ts +++ b/extensions/react-widget/src/types/index.ts @@ -39,4 +39,26 @@ export interface WidgetProps { collectFeedback?:boolean; deafultOpen?: boolean; showSources?: boolean + defaultOpen?: boolean; +} +export interface WidgetCoreProps extends WidgetProps { + widgetRef?:React.RefObject | null; + handleClose?:React.MouseEventHandler | undefined; + isOpen:boolean; + prefilledQuery?: string; +} + +export interface SearchBarProps { + apiHost?: string; + apiKey?: string; + theme?: THEME; + placeholder?: string; + width?: string; + buttonText?: string; +} + +export interface Result { + text:string; + title:string; + source:string; } \ No newline at end of file diff --git a/extensions/react-widget/src/utils/helper.ts b/extensions/react-widget/src/utils/helper.ts new file mode 100644 index 00000000..9f92fdcb --- /dev/null +++ b/extensions/react-widget/src/utils/helper.ts @@ -0,0 +1,151 @@ +export const getOS = () => { + const platform = window.navigator.platform; + const userAgent = window.navigator.userAgent || window.navigator.vendor; + + if (/Mac/i.test(platform)) { + return 'mac'; + } + + if (/Win/i.test(platform)) { + return 'win'; + } + + if (/Linux/i.test(platform) && !/Android/i.test(userAgent)) { + return 'linux'; + } + + if (/Android/i.test(userAgent)) { + return 'android'; + } + + if (/iPhone|iPad|iPod/i.test(userAgent)) { + return 'ios'; + } + + return 'other'; +}; + +interface ParsedElement { + content: string; + tag: string; +} + +export const processMarkdownString = (markdown: string, keyword?: string): ParsedElement[] => { + const lines = markdown.trim().split('\n'); + const keywordLower = keyword?.toLowerCase(); + + const escapeRegExp = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + const escapedKeyword = keyword ? escapeRegExp(keyword) : ''; + const keywordRegex = keyword ? new RegExp(`(${escapedKeyword})`, 'gi') : null; + + let isInCodeBlock = false; + let codeBlockContent: string[] = []; + let matchingLines: ParsedElement[] = []; + let firstLine: ParsedElement | null = null; + + for (let i = 0; i < lines.length; i++) { + const trimmedLine = lines[i].trim(); + if (!trimmedLine) continue; + + if (trimmedLine.startsWith('```')) { + if (!isInCodeBlock) { + isInCodeBlock = true; + codeBlockContent = []; + } else { + isInCodeBlock = false; + const codeContent = codeBlockContent.join('\n'); + const parsedElement: ParsedElement = { + content: codeContent, + tag: 'code' + }; + + if (!firstLine) { + firstLine = parsedElement; + } + + if (keywordLower && codeContent.toLowerCase().includes(keywordLower)) { + parsedElement.content = parsedElement.content.replace(keywordRegex!, '$1'); + matchingLines.push(parsedElement); + } + } + continue; + } + + if (isInCodeBlock) { + codeBlockContent.push(trimmedLine); + continue; + } + + let parsedElement: ParsedElement | null = null; + + const headingMatch = trimmedLine.match(/^(#{1,6})\s+(.+)$/); + const bulletMatch = trimmedLine.match(/^[-*]\s+(.+)$/); + const numberedMatch = trimmedLine.match(/^\d+\.\s+(.+)$/); + const blockquoteMatch = trimmedLine.match(/^>+\s*(.+)$/); + + let content = trimmedLine; + + if (headingMatch) { + content = headingMatch[2]; + parsedElement = { + content: content, + tag: 'heading' + }; + } else if (bulletMatch) { + content = bulletMatch[1]; + parsedElement = { + content: content, + tag: 'bulletList' + }; + } else if (numberedMatch) { + content = numberedMatch[1]; + parsedElement = { + content: content, + tag: 'numberedList' + }; + } else if (blockquoteMatch) { + content = blockquoteMatch[1]; + parsedElement = { + content: content, + tag: 'blockquote' + }; + } else { + parsedElement = { + content: content, + tag: 'text' + }; + } + + if (!firstLine) { + firstLine = parsedElement; + } + + if (keywordLower && parsedElement.content.toLowerCase().includes(keywordLower)) { + parsedElement.content = parsedElement.content.replace(keywordRegex!, '$1'); + matchingLines.push(parsedElement); + } + } + + if (isInCodeBlock && codeBlockContent.length > 0) { + const codeContent = codeBlockContent.join('\n'); + const parsedElement: ParsedElement = { + content: codeContent, + tag: 'code' + }; + + if (!firstLine) { + firstLine = parsedElement; + } + + if (keywordLower && codeContent.toLowerCase().includes(keywordLower)) { + parsedElement.content = parsedElement.content.replace(keywordRegex!, '$1'); + matchingLines.push(parsedElement); + } + } + + if (keywordLower && matchingLines.length > 0) { + return matchingLines; + } + + return firstLine ? [firstLine] : []; +}; diff --git a/extensions/react-widget/tsconfig.json b/extensions/react-widget/tsconfig.json index e73dd80f..88e86216 100644 --- a/extensions/react-widget/tsconfig.json +++ b/extensions/react-widget/tsconfig.json @@ -21,7 +21,7 @@ /* Linting */ "strict": true, "noUnusedLocals": false, - "noUnusedParameters": true, + "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, /* The "typeRoots" configuration specifies the locations where TypeScript looks for type definitions (.d.ts files) to diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 323a0d3c..88fb220b 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -18,6 +18,7 @@ module.exports = { }, plugins: ['react', 'unused-imports'], rules: { + 'react/prop-types': 'off', 'unused-imports/no-unused-imports': 'error', 'react/react-in-jsx-scope': 'off', 'prettier/prettier': [ diff --git a/frontend/index.html b/frontend/index.html index 5af1721b..30faadc9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,7 @@ - DocsGPT 🦖 + DocsGPT diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9973bb9e..d70a202f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,20 +8,21 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "@reduxjs/toolkit": "^2.2.7", + "@reduxjs/toolkit": "^2.5.1", "chart.js": "^4.4.4", - "i18next": "^23.15.1", - "i18next-browser-languagedetector": "^8.0.0", + "i18next": "^24.2.0", + "i18next-browser-languagedetector": "^8.0.2", "prop-types": "^15.8.1", "react": "^18.2.0", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", - "react-dropzone": "^14.2.3", - "react-i18next": "^15.0.2", + "react-dropzone": "^14.3.5", + "react-helmet": "^6.1.0", + "react-i18next": "^15.4.0", "react-markdown": "^9.0.1", "react-redux": "^8.0.5", - "react-router-dom": "^6.8.1", + "react-router-dom": "^7.1.1", "react-syntax-highlighter": "^15.5.0", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", @@ -30,28 +31,29 @@ "devDependencies": { "@types/react": "^18.0.27", "@types/react-dom": "^18.3.0", + "@types/react-helmet": "^6.1.11", "@types/react-syntax-highlighter": "^15.5.13", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.62.0", - "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.13", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-config-standard-with-typescript": "^34.0.0", - "eslint-plugin-import": "^2.30.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-n": "^15.7.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-promise": "^6.6.0", - "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react": "^7.37.3", "eslint-plugin-unused-imports": "^4.1.4", "husky": "^8.0.0", - "lint-staged": "^15.2.10", - "postcss": "^8.4.41", - "prettier": "^3.3.3", - "prettier-plugin-tailwindcss": "^0.6.8", - "tailwindcss": "^3.4.11", - "typescript": "^5.6.2", - "vite": "^5.4.6", + "lint-staged": "^15.3.0", + "postcss": "^8.4.49", + "prettier": "^3.4.2", + "prettier-plugin-tailwindcss": "^0.6.9", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite": "^5.4.14", "vite-plugin-svgr": "^4.2.0" } }, @@ -81,12 +83,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -94,30 +97,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", + "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -133,24 +136,25 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", "dev": true, "dependencies": { - "@babel/types": "^7.25.0", + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -162,14 +166,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -178,28 +182,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -209,89 +212,61 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "dev": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "dev": true, "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", "dev": true, "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.26.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -301,12 +276,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", - "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -316,12 +291,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", - "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -342,30 +317,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -374,14 +349,13 @@ } }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -890,18 +864,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -1067,9 +1029,10 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.7.tgz", - "integrity": "sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.1.tgz", + "integrity": "sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==", + "license": "MIT", "dependencies": { "immer": "^10.0.3", "redux": "^5.0.1", @@ -1077,7 +1040,7 @@ "reselect": "^5.1.0" }, "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "peerDependenciesMeta": { @@ -1089,14 +1052,6 @@ } } }, - "node_modules/@remix-run/router": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz", - "integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==", - "engines": { - "node": ">=14" - } - }, "node_modules/@rollup/pluginutils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", @@ -1587,6 +1542,11 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1675,7 +1635,17 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "devOptional": true, + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-helmet": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz", + "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==", + "dev": true, + "license": "MIT", "dependencies": { "@types/react": "*" } @@ -2089,14 +2059,14 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", - "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", "dev": true, "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, @@ -2104,7 +2074,7 @@ "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "node_modules/acorn": { @@ -2169,15 +2139,15 @@ } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, "engines": { - "node": ">=4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/any-promise": { @@ -2212,13 +2182,13 @@ "dev": true }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -2315,15 +2285,15 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2349,19 +2319,18 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -2371,9 +2340,9 @@ } }, "node_modules/attr-accept": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", - "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.4.tgz", + "integrity": "sha512-2pA6xFIbdTUDCAwjN8nQwI+842VwzbDUXO2IYlpPXQIORgKnavorcr4Ce3rwh+zsNg9zK7QPsdvDj3Lum4WX4w==", "engines": { "node": ">=4" } @@ -2442,12 +2411,15 @@ "dev": true }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/brace-expansion": { @@ -2473,9 +2445,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "dev": true, "funding": [ { @@ -2492,10 +2464,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -2547,16 +2519,44 @@ "dev": true }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -2596,9 +2596,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001688", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz", + "integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==", "dev": true, "funding": [ { @@ -2625,17 +2625,15 @@ } }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, "engines": { - "node": ">=4" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/character-entities": { @@ -2687,16 +2685,10 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2709,6 +2701,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -2756,21 +2751,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-convert/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", @@ -2813,6 +2793,14 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -2848,10 +2836,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2879,14 +2868,14 @@ "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2896,29 +2885,29 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -2930,11 +2919,11 @@ } }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3069,40 +3058,36 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, - "node_modules/easy-speech": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/easy-speech/-/easy-speech-2.4.0.tgz", - "integrity": "sha512-wpMv29DEoeP/eyXr4aXpDqd9DvlXl7aQs7BgfKbjGVxqkmQPgNmpbF5YULaTH5bc/5qrteg5MDfCD2Zd0qr4rQ==", - "funding": [ - { - "type": "GitHub", - "url": "https://github.com/sponsors/jankapunkt" - }, - { - "type": "PayPal", - "url": "https://paypal.me/kuesterjan" - } - ], - "engines": { - "node": ">= 14.x" - } - }, "node_modules/electron-to-chromium": { - "version": "1.5.11", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.11.tgz", - "integrity": "sha512-R1CccCDYqndR25CaXFd6hp/u9RaaMcftMkphmvuepXr5b1vfLkRml6aWVeBhXJ7rbevHkKEMJtz8XqPf7ffmew==", + "version": "1.5.73", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz", + "integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==", "dev": true }, "node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "node_modules/entities": { @@ -3138,57 +3123,62 @@ } }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -3198,13 +3188,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -3219,25 +3206,27 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", - "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.1.2" + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" }, "engines": { "node": ">= 0.4" @@ -3256,14 +3245,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3279,14 +3269,14 @@ } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -3334,23 +3324,14 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", @@ -3483,9 +3464,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz", - "integrity": "sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -3552,9 +3533,9 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz", - "integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "dependencies": { "@rtsao/scc": "^1.1.0", @@ -3565,7 +3546,7 @@ "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.9.0", + "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", @@ -3574,13 +3555,14 @@ "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -3708,28 +3690,28 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.35.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", - "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", + "version": "7.37.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz", + "integrity": "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==", "dev": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.2", + "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.19", + "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11", + "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "engines": { @@ -4172,20 +4154,20 @@ } }, "node_modules/file-selector": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", - "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.0.tgz", + "integrity": "sha512-ZuXAqGePcSPz4JuerOY06Dzzq0hrmQ6VGoXVzGyFI1npeOfBgqGIKKpznfYWRkSLJlXutkqVC5WvGZtkFVhu9Q==", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.7.0" }, "engines": { "node": ">= 12" } }, "node_modules/file-selector/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/fill-range": { "version": "7.1.1", @@ -4310,15 +4292,17 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -4346,9 +4330,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", - "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", "dev": true, "engines": { "node": ">=18" @@ -4358,16 +4342,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "dev": true, "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4376,6 +4365,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -4389,14 +4391,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -4447,12 +4449,13 @@ } }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -4482,12 +4485,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4506,21 +4509,15 @@ "dev": true }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-property-descriptors": { @@ -4536,10 +4533,13 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -4548,9 +4548,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "engines": { "node": ">= 0.4" @@ -4966,9 +4966,9 @@ } }, "node_modules/i18next": { - "version": "23.15.1", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.15.1.tgz", - "integrity": "sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==", + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz", + "integrity": "sha512-ArJJTS1lV6lgKH7yEf4EpgNZ7+THl7bsGxxougPYiXRTJ/Fe1j08/TBpV9QsXCIYVfdE/HWG/xLezJ5DOlfBOA==", "funding": [ { "type": "individual", @@ -4985,13 +4985,20 @@ ], "dependencies": { "@babel/runtime": "^7.23.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/i18next-browser-languagedetector": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", - "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", - "license": "MIT", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz", + "integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==", "dependencies": { "@babel/runtime": "^7.23.2" } @@ -5061,14 +5068,14 @@ "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==" }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5097,13 +5104,14 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -5119,12 +5127,15 @@ "dev": true }, "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.0.tgz", + "integrity": "sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5134,12 +5145,15 @@ } }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5158,13 +5172,13 @@ } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", + "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5201,11 +5215,13 @@ } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -5216,12 +5232,13 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5249,12 +5266,15 @@ } }, "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5273,12 +5293,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5320,18 +5343,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5342,12 +5353,13 @@ } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5377,13 +5389,15 @@ } }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -5405,12 +5419,12 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -5432,12 +5446,13 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5447,12 +5462,14 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5462,12 +5479,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -5489,25 +5506,28 @@ } }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz", + "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -5529,16 +5549,20 @@ "dev": true }, "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/jackspeak": { @@ -5583,15 +5607,15 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-parse-even-better-errors": { @@ -5638,14 +5662,13 @@ } }, "node_modules/katex": { - "version": "0.16.11", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", - "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], - "license": "MIT", "dependencies": { "commander": "^8.3.0" }, @@ -5676,12 +5699,15 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { @@ -5691,21 +5717,21 @@ "dev": true }, "node_modules/lint-staged": { - "version": "15.2.10", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", - "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.3.0.tgz", + "integrity": "sha512-vHFahytLoF2enJklgtOtCtIjZrKD/LoxlaUusd5nh7dWv/dkKQJY74ndFSzxCdv7g0ueGg1ORgTSt4Y9LPZn9A==", "dev": true, "dependencies": { - "chalk": "~5.3.0", + "chalk": "~5.4.1", "commander": "~12.1.0", - "debug": "~4.3.6", + "debug": "~4.4.0", "execa": "~8.0.1", - "lilconfig": "~3.1.2", - "listr2": "~8.2.4", + "lilconfig": "~3.1.3", + "listr2": "~8.2.5", "micromatch": "~4.0.8", "pidtree": "~0.6.0", "string-argv": "~0.3.2", - "yaml": "~2.5.0" + "yaml": "~2.6.1" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -5717,34 +5743,10 @@ "url": "https://opencollective.com/lint-staged" } }, - "node_modules/lint-staged/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/listr2": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", - "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", "dev": true, "dependencies": { "cli-truncate": "^4.0.0", @@ -5799,9 +5801,9 @@ } }, "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "engines": { "node": ">=12" @@ -5810,18 +5812,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/log-update/node_modules/is-fullwidth-code-point": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", @@ -5934,6 +5924,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", @@ -6979,9 +6978,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mz": { "version": "2.7.0", @@ -7041,9 +7040,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true }, "node_modules/normalize-path": { @@ -7109,9 +7108,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "dev": true, "engines": { "node": ">= 0.4" @@ -7130,14 +7129,16 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -7194,12 +7195,13 @@ } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -7251,6 +7253,23 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7420,9 +7439,9 @@ } }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -7477,9 +7496,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -7497,7 +7516,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -7575,18 +7594,6 @@ } } }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/postcss-nested": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", @@ -7641,9 +7648,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -7668,9 +7675,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.8.tgz", - "integrity": "sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA==", + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.9.tgz", + "integrity": "sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg==", "dev": true, "engines": { "node": ">=14.21.3" @@ -7814,12 +7821,12 @@ } }, "node_modules/react-chartjs-2": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", - "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", "peerDependencies": { "chart.js": "^4.1.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-copy-to-clipboard": { @@ -7847,12 +7854,12 @@ } }, "node_modules/react-dropzone": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", - "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "version": "14.3.5", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz", + "integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==", "dependencies": { - "attr-accept": "^2.2.2", - "file-selector": "^0.6.0", + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "engines": { @@ -7862,10 +7869,31 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-i18next": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.2.tgz", - "integrity": "sha512-z0W3/RES9Idv3MmJUcf0mDNeeMOUXe+xoL0kPfQPbDoZHmni/XsIoq5zgT2MCFUiau283GuBUK578uD/mkAbLQ==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz", + "integrity": "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==", "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" @@ -7974,33 +8002,50 @@ } }, "node_modules/react-router": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz", - "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.1.tgz", + "integrity": "sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==", "dependencies": { - "@remix-run/router": "1.3.2" + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" }, "engines": { - "node": ">=14" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8" + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/react-router-dom": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz", - "integrity": "sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.1.tgz", + "integrity": "sha512-vSrQHWlJ5DCfyrhgo0k6zViOe9ToK8uT5XGSmnuC2R3/g261IdIMpZVqfjD6vWSXdnf5Czs4VA/V60oVR6/jnA==", "dependencies": { - "@remix-run/router": "1.3.2", - "react-router": "6.8.1" + "react-router": "7.1.1" }, "engines": { - "node": ">=14" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/react-syntax-highlighter": { @@ -8053,18 +8098,19 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", - "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -8102,15 +8148,15 @@ "license": "MIT" }, "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -8397,14 +8443,15 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -8414,15 +8461,31 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -8448,6 +8511,11 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -8480,6 +8548,20 @@ "node": ">= 0.4" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8502,15 +8584,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -8556,18 +8692,6 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -8659,9 +8783,9 @@ } }, "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "engines": { "node": ">=12" @@ -8686,23 +8810,24 @@ } }, "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8722,15 +8847,18 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8740,15 +8868,19 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8947,18 +9079,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -9000,33 +9120,33 @@ "dev": true }, "node_modules/tailwindcss": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz", - "integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -9063,15 +9183,6 @@ "node": ">=0.8" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9158,6 +9269,11 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9183,30 +9299,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -9216,17 +9332,18 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -9236,17 +9353,17 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -9256,9 +9373,9 @@ } }, "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -9269,15 +9386,18 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9392,9 +9512,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -9411,8 +9531,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -9486,10 +9606,11 @@ } }, "node_modules/vite": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", - "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -9593,39 +9714,43 @@ } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-builtin-type": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", - "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "dependencies": { + "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", + "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", + "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", - "which-typed-array": "^1.1.15" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -9653,15 +9778,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "for-each": "^0.3.3", - "gopd": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -9772,9 +9898,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "engines": { "node": ">=12" @@ -9783,18 +9909,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -9831,9 +9945,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", "dev": true, "bin": { "yaml": "bin.mjs" diff --git a/frontend/package.json b/frontend/package.json index 83d531d6..89a55b04 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,20 +19,21 @@ ] }, "dependencies": { - "@reduxjs/toolkit": "^2.2.7", + "@reduxjs/toolkit": "^2.5.1", "chart.js": "^4.4.4", - "i18next": "^23.15.1", - "i18next-browser-languagedetector": "^8.0.0", + "i18next": "^24.2.0", + "i18next-browser-languagedetector": "^8.0.2", "prop-types": "^15.8.1", "react": "^18.2.0", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", - "react-dropzone": "^14.2.3", - "react-i18next": "^15.0.2", + "react-helmet": "^6.1.0", + "react-dropzone": "^14.3.5", + "react-i18next": "^15.4.0", "react-markdown": "^9.0.1", "react-redux": "^8.0.5", - "react-router-dom": "^6.8.1", + "react-router-dom": "^7.1.1", "react-syntax-highlighter": "^15.5.0", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", @@ -41,28 +42,29 @@ "devDependencies": { "@types/react": "^18.0.27", "@types/react-dom": "^18.3.0", + "@types/react-helmet": "^6.1.11", "@types/react-syntax-highlighter": "^15.5.13", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.62.0", - "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.13", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-config-standard-with-typescript": "^34.0.0", - "eslint-plugin-import": "^2.30.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-n": "^15.7.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-promise": "^6.6.0", - "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react": "^7.37.3", "eslint-plugin-unused-imports": "^4.1.4", "husky": "^8.0.0", - "lint-staged": "^15.2.10", - "postcss": "^8.4.41", - "prettier": "^3.3.3", - "prettier-plugin-tailwindcss": "^0.6.8", - "tailwindcss": "^3.4.11", - "typescript": "^5.6.2", - "vite": "^5.4.6", + "lint-staged": "^15.3.0", + "postcss": "^8.4.49", + "prettier": "^3.4.2", + "prettier-plugin-tailwindcss": "^0.6.9", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite": "^5.4.14", "vite-plugin-svgr": "^4.2.0" } } diff --git a/frontend/public/toolIcons/tool_api_tool.svg b/frontend/public/toolIcons/tool_api_tool.svg new file mode 100644 index 00000000..1e923cf3 --- /dev/null +++ b/frontend/public/toolIcons/tool_api_tool.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/public/toolIcons/tool_cryptoprice.svg b/frontend/public/toolIcons/tool_cryptoprice.svg new file mode 100644 index 00000000..6a422694 --- /dev/null +++ b/frontend/public/toolIcons/tool_cryptoprice.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/toolIcons/tool_postgres.svg b/frontend/public/toolIcons/tool_postgres.svg new file mode 100644 index 00000000..c7acdb18 --- /dev/null +++ b/frontend/public/toolIcons/tool_postgres.svg @@ -0,0 +1,29 @@ + + + + + + + Data + + + sql-database-generic + + + SQL Database (Generic) + + + image/svg+xml + + + Amido Limited + + + Richard Slater + + + + + + + \ No newline at end of file diff --git a/frontend/public/toolIcons/tool_telegram.svg b/frontend/public/toolIcons/tool_telegram.svg new file mode 100644 index 00000000..27536ded --- /dev/null +++ b/frontend/public/toolIcons/tool_telegram.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/Hero.tsx b/frontend/src/Hero.tsx index 644848dc..9fe965a1 100644 --- a/frontend/src/Hero.tsx +++ b/frontend/src/Hero.tsx @@ -37,12 +37,14 @@ export default function Hero({ ), diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index 242efb1a..bba83037 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -21,11 +21,10 @@ import { handleAbort, } from './conversation/conversationSlice'; import ConversationTile from './conversation/ConversationTile'; -import { useDarkTheme, useMediaQuery, useOutsideAlerter } from './hooks'; +import { useDarkTheme, useMediaQuery } from './hooks'; import useDefaultDocument from './hooks/useDefaultDocument'; import DeleteConvModal from './modals/DeleteConvModal'; import { ActiveState, Doc } from './models/misc'; -import APIKeyModal from './preferences/APIKeyModal'; import { getConversations, getDocs } from './preferences/preferenceApi'; import { selectApiKeyStatus, @@ -33,7 +32,6 @@ import { selectConversations, selectModalStateDeleteConv, selectSelectedDocs, - selectSelectedDocsStatus, selectSourceDocs, selectPaginatedDocuments, setConversations, @@ -52,21 +50,7 @@ interface NavigationProps { navOpen: boolean; setNavOpen: React.Dispatch>; } -/* const NavImage: React.FC<{ - Light: string | undefined; - Dark: string | undefined; -}> = ({ Light, Dark }) => { - return ( - <> - icon - icon - - ); -}; -NavImage.propTypes = { - Light: PropTypes.string, - Dark: PropTypes.string, -}; */ + export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { const dispatch = useDispatch(); const queries = useSelector(selectQueries); @@ -83,12 +67,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { const [isDocsListOpen, setIsDocsListOpen] = useState(false); const { t } = useTranslation(); const isApiKeySet = useSelector(selectApiKeyStatus); - const [apiKeyModalState, setApiKeyModalState] = - useState('INACTIVE'); - - const isSelectedDocsSet = useSelector(selectSelectedDocsStatus); - const [selectedDocsModalState, setSelectedDocsModalState] = - useState(isSelectedDocsSet ? 'INACTIVE' : 'ACTIVE'); const [uploadModalState, setUploadModalState] = useState('INACTIVE'); @@ -211,12 +189,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { console.error(err); }); } - useOutsideAlerter(navRef, () => { - if (isMobile && navOpen && apiKeyModalState === 'INACTIVE') { - setNavOpen(false); - setIsDocsListOpen(false); - } - }, [navOpen, isDocsListOpen, apiKeyModalState]); /* Needed to fix bug where if mobile nav was closed and then window was resized to desktop, nav would still be closed but the button to open would be gone, as per #1 on issue #146 @@ -239,7 +211,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { > menu toggle open new chat icon @@ -282,7 +254,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { }} > - + DocsGPT Logo

DocsGPT

@@ -294,7 +266,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { > menu toggle new

@@ -333,7 +305,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { Loading...

)} @@ -384,6 +356,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { Upload document { setUploadModalState('ACTIVE'); if (isMobile) { @@ -411,7 +384,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { > icon

@@ -433,7 +406,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { > discord @@ -446,7 +419,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { > x @@ -459,7 +432,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { > github @@ -476,28 +449,27 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { > menu toggle

DocsGPT
- - + {uploadModalState === 'ACTIVE' && ( + setUploadModalState('INACTIVE')} + > + )} ); } diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts index 4e7112d0..9bf659de 100644 --- a/frontend/src/api/endpoints.ts +++ b/frontend/src/api/endpoints.ts @@ -18,6 +18,18 @@ const endpoints = { FEEDBACK_ANALYTICS: '/api/get_feedback_analytics', LOGS: `/api/get_user_logs`, MANAGE_SYNC: '/api/manage_sync', + GET_AVAILABLE_TOOLS: '/api/available_tools', + GET_USER_TOOLS: '/api/get_tools', + CREATE_TOOL: '/api/create_tool', + UPDATE_TOOL_STATUS: '/api/update_tool_status', + UPDATE_TOOL: '/api/update_tool', + DELETE_TOOL: '/api/delete_tool', + GET_CHUNKS: (docId: string, page: number, per_page: number) => + `/api/get_chunks?id=${docId}&page=${page}&per_page=${per_page}`, + ADD_CHUNK: '/api/add_chunk', + DELETE_CHUNK: (docId: string, chunkId: string) => + `/api/delete_chunk?id=${docId}&chunk_id=${chunkId}`, + UPDATE_CHUNK: '/api/update_chunk', }, CONVERSATION: { ANSWER: '/api/answer', diff --git a/frontend/src/api/services/userService.ts b/frontend/src/api/services/userService.ts index 942318ae..e7f367f1 100644 --- a/frontend/src/api/services/userService.ts +++ b/frontend/src/api/services/userService.ts @@ -35,6 +35,30 @@ const userService = { apiClient.post(endpoints.USER.LOGS, data), manageSync: (data: any): Promise => apiClient.post(endpoints.USER.MANAGE_SYNC, data), + getAvailableTools: (): Promise => + apiClient.get(endpoints.USER.GET_AVAILABLE_TOOLS), + getUserTools: (): Promise => + apiClient.get(endpoints.USER.GET_USER_TOOLS), + createTool: (data: any): Promise => + apiClient.post(endpoints.USER.CREATE_TOOL, data), + updateToolStatus: (data: any): Promise => + apiClient.post(endpoints.USER.UPDATE_TOOL_STATUS, data), + updateTool: (data: any): Promise => + apiClient.post(endpoints.USER.UPDATE_TOOL, data), + deleteTool: (data: any): Promise => + apiClient.post(endpoints.USER.DELETE_TOOL, data), + getDocumentChunks: ( + docId: string, + page: number, + perPage: number, + ): Promise => + apiClient.get(endpoints.USER.GET_CHUNKS(docId, page, perPage)), + addChunk: (data: any): Promise => + apiClient.post(endpoints.USER.ADD_CHUNK, data), + deleteChunk: (docId: string, chunkId: string): Promise => + apiClient.delete(endpoints.USER.DELETE_CHUNK(docId, chunkId)), + updateChunk: (data: any): Promise => + apiClient.put(endpoints.USER.UPDATE_CHUNK, data), }; export default userService; diff --git a/frontend/src/assets/DragFileUpload.svg b/frontend/src/assets/DragFileUpload.svg new file mode 100644 index 00000000..1b41d193 --- /dev/null +++ b/frontend/src/assets/DragFileUpload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/chevron-down.svg b/frontend/src/assets/chevron-down.svg new file mode 100644 index 00000000..b2605251 --- /dev/null +++ b/frontend/src/assets/chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/circle-check.svg b/frontend/src/assets/circle-check.svg new file mode 100644 index 00000000..f0e8390d --- /dev/null +++ b/frontend/src/assets/circle-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/circle-x.svg b/frontend/src/assets/circle-x.svg new file mode 100644 index 00000000..d6bdd2c3 --- /dev/null +++ b/frontend/src/assets/circle-x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/cogwheel.svg b/frontend/src/assets/cogwheel.svg new file mode 100644 index 00000000..f5299b8b --- /dev/null +++ b/frontend/src/assets/cogwheel.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/no-files-dark.svg b/frontend/src/assets/no-files-dark.svg new file mode 100644 index 00000000..b1e28a0f --- /dev/null +++ b/frontend/src/assets/no-files-dark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/no-files.svg b/frontend/src/assets/no-files.svg new file mode 100644 index 00000000..36510546 --- /dev/null +++ b/frontend/src/assets/no-files.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/user.png b/frontend/src/assets/user.png new file mode 100644 index 00000000..3cc5d2f1 Binary files /dev/null and b/frontend/src/assets/user.png differ diff --git a/frontend/src/components/Accordion.tsx b/frontend/src/components/Accordion.tsx new file mode 100644 index 00000000..ec0a81d7 --- /dev/null +++ b/frontend/src/components/Accordion.tsx @@ -0,0 +1,59 @@ +import React, { useRef, useState } from 'react'; + +import ChevronDown from '../assets/chevron-down.svg'; + +type AccordionProps = { + title: string; + children: React.ReactNode; + className?: string; + titleClassName?: string; + contentClassName?: string; + open?: boolean; +}; + +export default function Accordion({ + title, + children, + className = '', + titleClassName = '', + contentClassName = '', + open: initialOpen = false, +}: AccordionProps) { + const contentRef = useRef(null); + const [isOpen, setIsOpen] = useState(initialOpen); + + const accordionContentStyle = { + height: isOpen ? 'auto' : '0px', + transition: 'height 0.3s ease-in-out, opacity 0.3s ease-in-out', + overflow: 'hidden', + } as React.CSSProperties; + + const toggleAccordion = () => { + setIsOpen(!isOpen); + }; + return ( +
+ + +
+ {children} +
+
+ ); +} diff --git a/frontend/src/components/Avatar.tsx b/frontend/src/components/Avatar.tsx index 202be207..7f8c5a93 100644 --- a/frontend/src/components/Avatar.tsx +++ b/frontend/src/components/Avatar.tsx @@ -5,7 +5,7 @@ export default function Avatar({ size, className, }: { - avatar: string | ReactNode; + avatar: ReactNode; size?: 'SMALL' | 'MEDIUM' | 'LARGE'; className: string; }) { diff --git a/frontend/src/components/CopyButton.tsx b/frontend/src/components/CopyButton.tsx index e13f9133..f0559f52 100644 --- a/frontend/src/components/CopyButton.tsx +++ b/frontend/src/components/CopyButton.tsx @@ -40,7 +40,7 @@ export default function CoppyButton({ /> ) : ( { handleCopyClick(text); }} diff --git a/frontend/src/components/DocumentPagination.tsx b/frontend/src/components/DocumentPagination.tsx index b0532362..6958d051 100644 --- a/frontend/src/components/DocumentPagination.tsx +++ b/frontend/src/components/DocumentPagination.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import SingleArrowLeft from '../assets/single-left-arrow.svg'; import SingleArrowRight from '../assets/single-right-arrow.svg'; import DoubleArrowLeft from '../assets/double-arrow-left.svg'; @@ -19,7 +20,11 @@ const Pagination: React.FC = ({ onPageChange, onRowsPerPageChange, }) => { - const [rowsPerPageOptions] = useState([5, 10, 15, 20]); + const { t } = useTranslation(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const rowsPerPageOptions = [5, 10, 20, 50]; + + const toggleDropdown = () => setIsDropdownOpen((prev) => !prev); const handlePreviousPage = () => { if (currentPage > 1) { @@ -41,31 +46,53 @@ const Pagination: React.FC = ({ onPageChange(totalPages); }; + const handleSelectRowsPerPage = (rows: number) => { + setIsDropdownOpen(false); + onRowsPerPageChange(rows); + }; + return (
-
- Rows per page: - + {/* Rows per page dropdown */} +
+ + {t('pagination.rowsPerPage')}: + +
+ +
+ {rowsPerPageOptions.map((option) => ( +
handleSelectRowsPerPage(option)} + className={`cursor-pointer px-4 py-2 text-xs hover:bg-gray-100 dark:hover:bg-neutral-700 ${ + rowsPerPage === option + ? 'bg-gray-100 dark:bg-neutral-700 dark:text-light-gray' + : 'bg-white dark:bg-dark-charcoal dark:text-light-gray' + }`} + > + {option} +
+ ))} +
+
+ {/* Pagination controls */}
- Page {currentPage} of {totalPages} + {t('pagination.pageOf', { currentPage, totalPages })}
-
@@ -85,7 +112,7 @@ const Pagination: React.FC = ({ > arrow @@ -96,7 +123,7 @@ const Pagination: React.FC = ({ > arrow @@ -107,7 +134,7 @@ const Pagination: React.FC = ({ > arrow diff --git a/frontend/src/components/DropdownMenu.tsx b/frontend/src/components/DropdownMenu.tsx index 787d3b84..2e6c922c 100644 --- a/frontend/src/components/DropdownMenu.tsx +++ b/frontend/src/components/DropdownMenu.tsx @@ -48,7 +48,7 @@ export default function DropdownMenu({
@@ -66,16 +68,22 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
{tabs.map((tab, index) => ( @@ -84,7 +92,8 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
diff --git a/frontend/src/components/SkeletonLoader.tsx b/frontend/src/components/SkeletonLoader.tsx index e9a136e4..b73c5835 100644 --- a/frontend/src/components/SkeletonLoader.tsx +++ b/frontend/src/components/SkeletonLoader.tsx @@ -1,8 +1,14 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; interface SkeletonLoaderProps { count?: number; - component?: 'default' | 'analysis' | 'chatbot' | 'logs'; + component?: + | 'default' + | 'analysis' + | 'logs' + | 'table' + | 'chatbot' + | 'dropdown'; } const SkeletonLoader: React.FC = ({ @@ -32,107 +38,162 @@ const SkeletonLoader: React.FC = ({ }; }, [count]); - return ( -
- {component === 'default' ? ( - [...Array(skeletonCount)].map((_, idx) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )) - ) : component === 'analysis' ? ( - [...Array(skeletonCount)].map((_, idx) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )) - ) : component === 'chatbot' ? ( -
-
-
-
-
-
-
-
+ const renderTable = () => ( + <> + {[...Array(4)].map((_, idx) => ( + + +
+ + +
+ + +
+ + +
+ + + ))} + + ); - {[...Array(skeletonCount * 6)].map((_, idx) => ( -
-
-
-
-
-
- ))} -
- ) : ( - [...Array(skeletonCount)].map((_, idx) => ( -
-
-
-
-
-
-
-
-
- )) - )} + const renderChatbot = () => ( + <> + {[...Array(4)].map((_, idx) => ( + + +
+ + +
+ + +
+ + +
+ + + ))} + + ); + + const renderDropdown = () => ( +
+
+
+
+
+
); + + const renderLogs = () => ( +
+ {[...Array(8)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+ ))} +
+ ); + + const renderDefault = () => ( + <> + {[...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} + + ); + + const renderAnalysis = () => ( + <> + {[...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} + + ); + + const componentMap = { + table: renderTable, + chatbot: renderChatbot, + dropdown: renderDropdown, + logs: renderLogs, + default: renderDefault, + analysis: renderAnalysis, + }; + + const render = componentMap[component] || componentMap.default; + + return <>{render()}; }; export default SkeletonLoader; diff --git a/frontend/src/components/Spinner.tsx b/frontend/src/components/Spinner.tsx new file mode 100644 index 00000000..d34a5665 --- /dev/null +++ b/frontend/src/components/Spinner.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +type SpinnerProps = { + size?: 'small' | 'medium' | 'large'; + color?: string; +}; + +export default function Spinner({ + size = 'medium', + color = 'grey', +}: SpinnerProps) { + const sizeMap = { + small: '20px', + medium: '30px', + large: '40px', + }; + const spinnerSize = sizeMap[size]; + + const spinnerStyle = { + width: spinnerSize, + height: spinnerSize, + aspectRatio: '1', + borderRadius: '50%', + background: ` + radial-gradient(farthest-side, ${color} 94%, #0000) top/8px 8px no-repeat, + conic-gradient(#0000 30%, ${color}) + `, + WebkitMask: + 'radial-gradient(farthest-side, #0000 calc(100% - 8px), #000 0)', + animation: 'l13 1s infinite linear', + } as React.CSSProperties; + + const keyframesStyle = `@keyframes l13 { + 100% { transform: rotate(1turn) } + }`; + + return ( + <> + +
+ + ); +} diff --git a/frontend/src/components/ToggleSwitch.tsx b/frontend/src/components/ToggleSwitch.tsx new file mode 100644 index 00000000..061d2563 --- /dev/null +++ b/frontend/src/components/ToggleSwitch.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +type ToggleSwitchProps = { + checked: boolean; + onChange: (checked: boolean) => void; + className?: string; + label?: string; + disabled?: boolean; + activeColor?: string; + inactiveColor?: string; + id?: string; +}; + +const ToggleSwitch: React.FC = ({ + checked, + onChange, + className = '', + label, + disabled = false, + activeColor = 'bg-purple-30', + inactiveColor = 'bg-transparent', + id, +}) => { + return ( + + ); +}; + +export default ToggleSwitch; diff --git a/frontend/src/components/types/index.ts b/frontend/src/components/types/index.ts index 7af1c545..1d4b3a61 100644 --- a/frontend/src/components/types/index.ts +++ b/frontend/src/components/types/index.ts @@ -8,6 +8,8 @@ export type InputProps = { maxLength?: number; name?: string; placeholder?: string; + label?: string; + required?: boolean; className?: string; children?: React.ReactElement; onChange: ( diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index b2f24b5a..29438f93 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -1,8 +1,10 @@ -import { Fragment, useEffect, useRef, useState } from 'react'; +import { Fragment, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import Hero from '../Hero'; +import { useDropzone } from 'react-dropzone'; +import DragFileUpload from '../assets/DragFileUpload.svg'; import ArrowDown from '../assets/arrow-down.svg'; import newChatIcon from '../assets/openNewChat.svg'; import Send from '../assets/send.svg'; @@ -21,12 +23,15 @@ import { FEEDBACK, Query } from './conversationModels'; import { addQuery, fetchAnswer, + resendQuery, selectQueries, selectStatus, setConversation, updateConversationId, updateQuery, } from './conversationSlice'; +import Upload from '../upload/Upload'; +import { ActiveState } from '../models/misc'; export default function Conversation() { const queries = useSelector(selectQueries); @@ -44,6 +49,47 @@ export default function Conversation() { const [isShareModalOpen, setShareModalState] = useState(false); const { t } = useTranslation(); const { isMobile } = useMediaQuery(); + const [uploadModalState, setUploadModalState] = + useState('INACTIVE'); + const [files, setFiles] = useState([]); + const [handleDragActive, setHandleDragActive] = useState(false); + + const onDrop = useCallback((acceptedFiles: File[]) => { + setUploadModalState('ACTIVE'); + setFiles(acceptedFiles); + setHandleDragActive(false); + }, []); + + const { getRootProps, getInputProps } = useDropzone({ + onDrop, + noClick: true, + multiple: true, + onDragEnter: () => { + setHandleDragActive(true); + }, + onDragLeave: () => { + setHandleDragActive(false); + }, + maxSize: 25000000, + accept: { + 'application/pdf': ['.pdf'], + 'text/plain': ['.txt'], + 'text/x-rst': ['.rst'], + 'text/x-markdown': ['.md'], + 'application/zip': ['.zip'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + ['.docx'], + 'application/json': ['.json'], + 'text/csv': ['.csv'], + 'text/html': ['.html'], + 'application/epub+zip': ['.epub'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [ + '.xlsx', + ], + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + ['.pptx'], + }, + }); const handleUserInterruption = () => { if (!eventInterrupt && status === 'loading') setEventInterrupt(true); @@ -85,27 +131,57 @@ export default function Conversation() { const handleQuestion = ({ question, isRetry = false, + updated = null, + indx = undefined, }: { question: string; isRetry?: boolean; + updated?: boolean | null; + indx?: number; }) => { - question = question.trim(); - if (question === '') return; - setEventInterrupt(false); - !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries - fetchStream.current = dispatch(fetchAnswer({ question })); + if (updated === true) { + !isRetry && + dispatch(resendQuery({ index: indx as number, prompt: question })); //dispatch only new queries + fetchStream.current = dispatch(fetchAnswer({ question, indx })); + } else { + question = question.trim(); + if (question === '') return; + setEventInterrupt(false); + !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries + fetchStream.current = dispatch(fetchAnswer({ question })); + } }; const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => { const prevFeedback = query.feedback; dispatch(updateQuery({ index, query: { feedback } })); - handleSendFeedback(query.prompt, query.response!, feedback).catch(() => - dispatch(updateQuery({ index, query: { feedback: prevFeedback } })), + handleSendFeedback( + query.prompt, + query.response!, + feedback, + conversationId as string, + index, + ).catch(() => + handleSendFeedback( + query.prompt, + query.response!, + feedback, + conversationId as string, + index, + ).catch(() => + dispatch(updateQuery({ index, query: { feedback: prevFeedback } })), + ), ); }; - const handleQuestionSubmission = () => { - if (inputRef.current?.value && status !== 'loading') { + const handleQuestionSubmission = ( + updatedQuestion?: string, + updated?: boolean, + indx?: number, + ) => { + if (updated === true) { + handleQuestion({ question: updatedQuestion as string, updated, indx }); + } else if (inputRef.current?.value && status !== 'loading') { if (lastQueryReturnedErr) { // update last failed query with new prompt dispatch( @@ -149,6 +225,7 @@ export default function Conversation() { message={query.response} type={'ANSWER'} sources={query.sources} + toolCalls={query.tool_calls} feedback={query.feedback} handleFeedback={(feedback: FEEDBACK) => handleFeedback(query, feedback, index) @@ -290,6 +367,8 @@ export default function Conversation() { key={`${index}QUESTION`} message={query.prompt} type="QUESTION" + handleUpdatedQuestionSubmission={handleQuestionSubmission} + questionNumber={index} sources={query.sources} > @@ -303,14 +382,24 @@ export default function Conversation() { )}
-
-
+
+
+ + + {status === 'loading' ? ( + alt={t('loading')} + /> ) : (
- +
)}
@@ -339,6 +436,26 @@ export default function Conversation() { {t('tagline')}

+ {handleDragActive && ( +
+ + + {t('modals.uploadDoc.drag.title')} + + + {t('modals.uploadDoc.drag.description')} + +
+ )} + {uploadModalState === 'ACTIVE' && ( + setUploadModalState('INACTIVE')} + > + )}
); } diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index 8e5df666..d60b531a 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -1,6 +1,7 @@ import 'katex/dist/katex.min.css'; -import { forwardRef, useState } from 'react'; +import { forwardRef, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import ReactMarkdown from 'react-markdown'; import { useSelector } from 'react-redux'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; @@ -10,21 +11,27 @@ import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import DocsGPT3 from '../assets/cute_docsgpt3.svg'; +import ChevronDown from '../assets/chevron-down.svg'; import Dislike from '../assets/dislike.svg?react'; import Document from '../assets/document.svg'; +import Edit from '../assets/edit.svg'; import Like from '../assets/like.svg?react'; import Link from '../assets/link.svg'; import Sources from '../assets/sources.svg'; +import UserIcon from '../assets/user.png'; +import Accordion from '../components/Accordion'; import Avatar from '../components/Avatar'; import CopyButton from '../components/CopyButton'; import Sidebar from '../components/Sidebar'; import SpeakButton from '../components/TextToSpeechButton'; +import { useOutsideAlerter } from '../hooks'; import { selectChunks, selectSelectedDocs, } from '../preferences/preferenceSlice'; import classes from './ConversationBubble.module.css'; import { FEEDBACK, MESSAGE_TYPE } from './conversationModels'; +import { ToolCallsType } from './types'; const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false; @@ -37,37 +44,118 @@ const ConversationBubble = forwardRef< feedback?: FEEDBACK; handleFeedback?: (feedback: FEEDBACK) => void; sources?: { title: string; text: string; source: string }[]; + toolCalls?: ToolCallsType[]; retryBtn?: React.ReactElement; + questionNumber?: number; + handleUpdatedQuestionSubmission?: ( + updatedquestion?: string, + updated?: boolean, + index?: number, + ) => void; } >(function ConversationBubble( - { message, type, className, feedback, handleFeedback, sources, retryBtn }, + { + message, + type, + className, + feedback, + handleFeedback, + sources, + toolCalls, + retryBtn, + questionNumber, + handleUpdatedQuestionSubmission, + }, ref, ) { + const { t } = useTranslation(); // const bubbleRef = useRef(null); const chunks = useSelector(selectChunks); const selectedDocs = useSelector(selectSelectedDocs); const [isLikeHovered, setIsLikeHovered] = useState(false); + const [isEditClicked, setIsEditClicked] = useState(false); const [isDislikeHovered, setIsDislikeHovered] = useState(false); + const [isQuestionHovered, setIsQuestionHovered] = useState(false); + const [editInputBox, setEditInputBox] = useState(''); + const [isLikeClicked, setIsLikeClicked] = useState(false); const [isDislikeClicked, setIsDislikeClicked] = useState(false); const [activeTooltip, setActiveTooltip] = useState(null); const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const editableQueryRef = useRef(null); + useOutsideAlerter(editableQueryRef, () => setIsEditClicked(false), [], true); + const handleEditClick = () => { + setIsEditClicked(false); + handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber); + }; let bubble; if (type === 'QUESTION') { bubble = (
setIsQuestionHovered(true)} + onMouseLeave={() => setIsQuestionHovered(false)} > -
- {message} + + } + /> + {!isEditClicked && ( + <> +
+ {message} +
+ + + )} + {isEditClicked && ( +
+ +
+
+
+ + +
+
+
+ +
+ ); + } else { + return ( +
+
+
+ +
+

+ Edit Chunk +

+
+ + Title + + setTitle(e.target.value)} + borderVariant="thin" + placeholder={'Enter title'} + > +
+
+
+ + Body text + + +
+
+
+ +
+ + +
+
+
+
+
+ { + /* no-op */ + } + } + submitLabel="Delete" + /> +
+ ); + } +} diff --git a/frontend/src/modals/ConfigToolModal.tsx b/frontend/src/modals/ConfigToolModal.tsx new file mode 100644 index 00000000..f26029fc --- /dev/null +++ b/frontend/src/modals/ConfigToolModal.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import Exit from '../assets/exit.svg'; +import Input from '../components/Input'; +import { ActiveState } from '../models/misc'; +import { AvailableToolType } from './types'; +import userService from '../api/services/userService'; + +export default function ConfigToolModal({ + modalState, + setModalState, + tool, + getUserTools, +}: { + modalState: ActiveState; + setModalState: (state: ActiveState) => void; + tool: AvailableToolType | null; + getUserTools: () => void; +}) { + const { t } = useTranslation(); + const [authKey, setAuthKey] = React.useState(''); + + const handleAddTool = (tool: AvailableToolType) => { + userService + .createTool({ + name: tool.name, + displayName: tool.displayName, + description: tool.description, + config: { token: authKey }, + actions: tool.actions, + status: true, + }) + .then(() => { + setModalState('INACTIVE'); + getUserTools(); + }); + }; + return ( +
+
+
+ +
+

+ {t('modals.configTool.title')} +

+

+ {t('modals.configTool.type')}:{' '} + {tool?.name} +

+
+ + {t('modals.configTool.apiKeyLabel')} + + setAuthKey(e.target.value)} + borderVariant="thin" + placeholder={t('modals.configTool.apiKeyPlaceholder')} + > +
+
+ + +
+
+
+
+
+ ); +} diff --git a/frontend/src/modals/ConfirmationModal.tsx b/frontend/src/modals/ConfirmationModal.tsx index 0b39440b..1a2e22e2 100644 --- a/frontend/src/modals/ConfirmationModal.tsx +++ b/frontend/src/modals/ConfirmationModal.tsx @@ -1,7 +1,9 @@ -import Exit from '../assets/exit.svg'; -import { ActiveState } from '../models/misc'; import { useTranslation } from 'react-i18next'; -function ConfirmationModal({ + +import { ActiveState } from '../models/misc'; +import WrapperModal from './WrapperModal'; + +export default function ConfirmationModal({ message, modalState, setModalState, @@ -20,50 +22,42 @@ function ConfirmationModal({ }) { const { t } = useTranslation(); return ( -
-
-
- -
-

- {message} -

+ <> + {modalState === 'ACTIVE' && ( + { + setModalState('INACTIVE'); + handleCancel && handleCancel(); + }} + > +
-
- - +

+ {message} +

+
+
+ + +
-
-
-
+ + )} + ); } - -export default ConfirmationModal; diff --git a/frontend/src/modals/CreateAPIKeyModal.tsx b/frontend/src/modals/CreateAPIKeyModal.tsx index eb085a28..5c8c75b8 100644 --- a/frontend/src/modals/CreateAPIKeyModal.tsx +++ b/frontend/src/modals/CreateAPIKeyModal.tsx @@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import userService from '../api/services/userService'; -import Exit from '../assets/exit.svg'; import Dropdown from '../components/Dropdown'; import Input from '../components/Input'; import { CreateAPIKeyModalProps, Doc } from '../models/misc'; import { selectSourceDocs } from '../preferences/preferenceSlice'; +import WrapperModal from './WrapperModal'; const embeddingsName = import.meta.env.VITE_EMBEDDINGS_NAME || @@ -73,91 +73,82 @@ export default function CreateAPIKeyModal({ handleFetchPrompts(); }, []); return ( -
-
- -
- - {t('modals.createAPIKey.label')} - -
-
- - {t('modals.createAPIKey.apiKeyName')} - - setAPIKeyName(e.target.value)} - > -
-
- { - setSourcePath(selection); - }} - options={extractDocPaths()} - size="w-full" - rounded="xl" - border="border" - /> -
-
- - setPrompt(value) - } - size="w-full" - border="border" - /> -
-
-

- {t('modals.createAPIKey.chunks')} -

- setChunk(value)} - size="w-full" - border="border" - /> -
- + +
+ + {t('modals.createAPIKey.label')} +
-
+
+ + {t('modals.createAPIKey.apiKeyName')} + + setAPIKeyName(e.target.value)} + > +
+
+ { + setSourcePath(selection); + }} + options={extractDocPaths()} + size="w-full" + rounded="xl" + border="border" + /> +
+
+ + setPrompt(value) + } + size="w-full" + border="border" + /> +
+
+

+ {t('modals.createAPIKey.chunks')} +

+ setChunk(value)} + size="w-full" + border="border" + /> +
+ + ); } diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index fbb49468..44156761 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -10,7 +10,6 @@ import { import Dropdown from '../components/Dropdown'; import { Doc } from '../models/misc'; import Spinner from '../assets/spinner.svg'; -import Exit from '../assets/exit.svg'; const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; const embeddingsName = import.meta.env.VITE_EMBEDDINGS_NAME || @@ -19,6 +18,7 @@ const embeddingsName = type StatusType = 'loading' | 'idle' | 'fetched' | 'failed'; import conversationService from '../api/services/conversationService'; +import WrapperModal from './WrapperModal'; export const ShareConversationModal = ({ close, @@ -99,85 +99,84 @@ export const ShareConversationModal = ({ }; return ( -
-
- -
-

{t('modals.shareConv.label')}

-

{t('modals.shareConv.note')}

-
- {t('modals.shareConv.option')} - -
- {allowPrompt && ( -
- - setSourcePath(selection) - } - options={extractDocPaths(sourceDocs ?? [])} - size="w-full" - rounded="xl" + +
+

+ {t('modals.shareConv.label')} +

+

+ {t('modals.shareConv.note')} +

+
+ + {t('modals.shareConv.option')} + +
-
+
); }; diff --git a/frontend/src/modals/WrapperModal.tsx b/frontend/src/modals/WrapperModal.tsx new file mode 100644 index 00000000..dcf1bebb --- /dev/null +++ b/frontend/src/modals/WrapperModal.tsx @@ -0,0 +1,57 @@ +import React, { useEffect, useRef } from 'react'; + +import Exit from '../assets/exit.svg'; +import { WrapperModalPropsType } from './types'; + +export default function WrapperModal({ + children, + close, + isPerformingTask, +}: WrapperModalPropsType) { + const modalRef = useRef(null); + + useEffect(() => { + if (isPerformingTask) return; + const handleClickOutside = (event: MouseEvent) => { + if ( + modalRef.current && + !modalRef.current.contains(event.target as Node) + ) { + close(); + } + }; + + const handleEscapePress = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + close(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleEscapePress); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleEscapePress); + }; + }, [close]); + + return ( +
+
+ {!isPerformingTask && ( + + )} + {children} +
+
+ ); +} diff --git a/frontend/src/modals/types/index.ts b/frontend/src/modals/types/index.ts new file mode 100644 index 00000000..0e3c76ab --- /dev/null +++ b/frontend/src/modals/types/index.ts @@ -0,0 +1,18 @@ +export type AvailableToolType = { + name: string; + displayName: string; + description: string; + configRequirements: object; + actions: { + name: string; + description: string; + parameters: object; + }[]; +}; + +export type WrapperModalPropsType = { + children?: React.ReactNode; + isPerformingTask?: boolean; + close: () => void; + className?: string; +}; diff --git a/frontend/src/preferences/PromptsModal.tsx b/frontend/src/preferences/PromptsModal.tsx index 3aa8c54c..11cb0685 100644 --- a/frontend/src/preferences/PromptsModal.tsx +++ b/frontend/src/preferences/PromptsModal.tsx @@ -2,6 +2,7 @@ import { ActiveState } from '../models/misc'; import Exit from '../assets/exit.svg'; import Input from '../components/Input'; import React from 'react'; +import { useTranslation } from 'react-i18next'; function AddPrompt({ setModalState, @@ -20,6 +21,8 @@ function AddPrompt({ setNewPromptContent: (content: string) => void; disableSave: boolean; }) { + const { t } = useTranslation(); + return (

- Add Prompt + {t('modals.prompts.addPrompt')}

- Add your custom prompt and save it to DocsGPT + {t('modals.prompts.addDescription')}

+
- Prompt Name + {t('modals.prompts.promptName')}
- Prompt Text + {t('modals.prompts.promptText')}
+
@@ -68,9 +80,11 @@ function AddPrompt({ onClick={handleAddPrompt} className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:opacity-90" disabled={disableSave} - title={disableSave && newPromptName ? 'Name already exists' : ''} + title={ + disableSave && newPromptName ? t('modals.prompts.nameExists') : '' + } > - Save + {t('modals.prompts.save')}
@@ -97,6 +111,8 @@ function EditPrompt({ currentPromptEdit: { name: string; id: string; type: string }; disableSave: boolean; }) { + const { t } = useTranslation(); + return (

- Edit Prompt + {t('modals.prompts.editPrompt')}

- Edit your custom prompt and save it to DocsGPT + {t('modals.prompts.editDescription')}

+ setEditPromptName(e.target.value)} - > + />
- Prompt Name + {t('modals.prompts.promptName')}
- Prompt Text + {t('modals.prompts.promptText')}
+
@@ -150,9 +175,13 @@ function EditPrompt({ handleEditPrompt(currentPromptEdit.id, currentPromptEdit.type); }} disabled={currentPromptEdit.type === 'public' || disableSave} - title={disableSave && editPromptName ? 'Name already exists' : ''} + title={ + disableSave && editPromptName + ? t('modals.prompts.nameExists') + : '' + } > - Save + {t('modals.prompts.save')}
diff --git a/frontend/src/preferences/preferenceApi.ts b/frontend/src/preferences/preferenceApi.ts index 32cf8b17..8d21bdcd 100644 --- a/frontend/src/preferences/preferenceApi.ts +++ b/frontend/src/preferences/preferenceApi.ts @@ -25,9 +25,10 @@ export async function getDocsWithPagination( order = 'desc', pageNumber = 1, rowsPerPage = 10, + searchTerm = '', ): Promise { try { - const query = `sort=${sort}&order=${order}&page=${pageNumber}&rows=${rowsPerPage}`; + const query = `sort=${sort}&order=${order}&page=${pageNumber}&rows=${rowsPerPage}&search=${searchTerm}`; const response = await userService.getDocsWithPagination(query); const data = await response.json(); const docs: Doc[] = []; diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index b039477c..642566f1 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -5,16 +5,22 @@ import userService from '../api/services/userService'; import Trash from '../assets/trash.svg'; import CreateAPIKeyModal from '../modals/CreateAPIKeyModal'; import SaveAPIKeyModal from '../modals/SaveAPIKeyModal'; +import ConfirmationModal from '../modals/ConfirmationModal'; import { APIKeyData } from './types'; import SkeletonLoader from '../components/SkeletonLoader'; +import { useLoaderState } from '../hooks'; export default function APIKeys() { const { t } = useTranslation(); - const [isCreateModalOpen, setCreateModal] = React.useState(false); - const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false); - const [newKey, setNewKey] = React.useState(''); - const [apiKeys, setApiKeys] = React.useState([]); - const [loading, setLoading] = useState(true); + const [isCreateModalOpen, setCreateModal] = useState(false); + const [isSaveKeyModalOpen, setSaveKeyModal] = useState(false); + const [newKey, setNewKey] = useState(''); + const [apiKeys, setApiKeys] = useState([]); + const [loading, setLoading] = useLoaderState(true); + const [keyToDelete, setKeyToDelete] = useState<{ + id: string; + name: string; + } | null>(null); const handleFetchKeys = async () => { setLoading(true); @@ -33,6 +39,7 @@ export default function APIKeys() { }; const handleDeleteKey = (id: string) => { + setLoading(true); userService .deleteAPIKey({ id }) .then((response) => { @@ -42,11 +49,16 @@ export default function APIKeys() { return response.json(); }) .then((data) => { - data.success === true && + if (data.success === true) { setApiKeys((previous) => previous.filter((elem) => elem.id !== id)); + } + setKeyToDelete(null); }) .catch((error) => { console.error(error); + }) + .finally(() => { + setLoading(false); }); }; @@ -57,6 +69,7 @@ export default function APIKeys() { prompt_id: string; chunks: string; }) => { + setLoading(true); userService .createAPIKey(payload) .then((response) => { @@ -74,6 +87,9 @@ export default function APIKeys() { }) .catch((error) => { console.error(error); + }) + .finally(() => { + setLoading(false); }); }; @@ -104,47 +120,86 @@ export default function APIKeys() { close={() => setSaveKeyModal(false)} /> )} + {keyToDelete && ( + setKeyToDelete(null)} + submitLabel={t('modals.deleteConv.delete')} + handleSubmit={() => handleDeleteKey(keyToDelete.id)} + handleCancel={() => setKeyToDelete(null)} + /> + )}
- {loading ? ( - - ) : ( - - - - - - - - - - - {!apiKeys?.length && ( - - - - )} - {apiKeys?.map((element, index) => ( - - - - - - - ))} - -
{t('settings.apiKeys.name')}{t('settings.apiKeys.sourceDoc')}{t('settings.apiKeys.key')}
- {t('settings.apiKeys.noData')} -
{element.name}{element.source}{element.key} - Delete handleDeleteKey(element.id)} - /> -
- )} +
+
+
+ + + + + + + + + + + {loading ? ( + + ) : !apiKeys?.length ? ( + + + + ) : ( + Array.isArray(apiKeys) && + apiKeys.map((element, index) => ( + + + + + + + )) + )} + +
+ {t('settings.apiKeys.name')} + + {t('settings.apiKeys.sourceDoc')} + + {t('settings.apiKeys.key')} +
+ {t('settings.apiKeys.noData')} +
{element.name}{element.source}{element.key} + {`Delete + setKeyToDelete({ + id: element.id, + name: element.name, + }) + } + /> +
+
+
+
diff --git a/frontend/src/settings/Analytics.tsx b/frontend/src/settings/Analytics.tsx index 8baad361..4d390495 100644 --- a/frontend/src/settings/Analytics.tsx +++ b/frontend/src/settings/Analytics.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { BarElement, CategoryScale, @@ -15,6 +16,7 @@ import Dropdown from '../components/Dropdown'; import { htmlLegendPlugin } from '../utils/chartUtils'; import { formatDate } from '../utils/dateTimeUtils'; import { APIKeyData } from './types'; +import { useLoaderState } from '../hooks'; import type { ChartData } from 'chart.js'; import SkeletonLoader from '../components/SkeletonLoader'; @@ -28,15 +30,29 @@ ChartJS.register( Legend, ); -const filterOptions = [ - { label: 'Hour', value: 'last_hour' }, - { label: '24 Hours', value: 'last_24_hour' }, - { label: '7 Days', value: 'last_7_days' }, - { label: '15 Days', value: 'last_15_days' }, - { label: '30 Days', value: 'last_30_days' }, -]; - export default function Analytics() { + const { t } = useTranslation(); + + const filterOptions = [ + { label: t('settings.analytics.filterOptions.hour'), value: 'last_hour' }, + { + label: t('settings.analytics.filterOptions.last24Hours'), + value: 'last_24_hour', + }, + { + label: t('settings.analytics.filterOptions.last7Days'), + value: 'last_7_days', + }, + { + label: t('settings.analytics.filterOptions.last15Days'), + value: 'last_15_days', + }, + { + label: t('settings.analytics.filterOptions.last30Days'), + value: 'last_30_days', + }, + ]; + const [messagesData, setMessagesData] = useState({ label: '30 Days', value: 'last_30_days' }); + }>({ + label: t('settings.analytics.filterOptions.last30Days'), + value: 'last_30_days', + }); const [tokenUsageFilter, setTokenUsageFilter] = useState<{ label: string; value: string; - }>({ label: '30 Days', value: 'last_30_days' }); + }>({ + label: t('settings.analytics.filterOptions.last30Days'), + value: 'last_30_days', + }); const [feedbackFilter, setFeedbackFilter] = useState<{ label: string; value: string; - }>({ label: '30 Days', value: 'last_30_days' }); + }>({ + label: t('settings.analytics.filterOptions.last30Days'), + value: 'last_30_days', + }); - const [loadingMessages, setLoadingMessages] = useState(true); - const [loadingTokens, setLoadingTokens] = useState(true); - const [loadingFeedback, setLoadingFeedback] = useState(true); + const [loadingMessages, setLoadingMessages] = useLoaderState(true); + const [loadingTokens, setLoadingTokens] = useLoaderState(true); + const [loadingFeedback, setLoadingFeedback] = useLoaderState(true); const fetchChatbots = async () => { try { @@ -165,7 +190,7 @@ export default function Analytics() {

- Filter by chatbot + {t('settings.analytics.filterByChatbot')}

{ setSelectedChatbot( chatbots.find((item) => item.id === chatbot.value), @@ -191,6 +216,7 @@ export default function Analytics() { } rounded="3xl" border="border" + borderColor="gray-700" />
@@ -199,12 +225,12 @@ export default function Analytics() {

- Messages + {t('settings.analytics.messages')}

- Token Usage + {t('settings.analytics.tokenUsage')}

- Feedback + {t('settings.analytics.feedback')}

item.positive, ), backgroundColor: '#7D54D1', }, { - label: 'Negative Feedback', + label: t('settings.analytics.negativeFeedback'), data: Object.values(feedbackData || {}).map( (item) => item.negative, ), diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index f91a3355..e72a6e16 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -1,22 +1,33 @@ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import userService from '../api/services/userService'; -import SyncIcon from '../assets/sync.svg'; -import Trash from '../assets/trash.svg'; -import caretSort from '../assets/caret-sort.svg'; -import DropdownMenu from '../components/DropdownMenu'; -import SkeletonLoader from '../components/SkeletonLoader'; -import Input from '../components/Input'; -import Upload from '../upload/Upload'; // Import the Upload component -import Pagination from '../components/DocumentPagination'; +import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; -import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure ActiveState type is imported -import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi'; -import { setSourceDocs } from '../preferences/preferenceSlice'; -import { setPaginatedDocuments } from '../preferences/preferenceSlice'; -// Utility function to format numbers +import userService from '../api/services/userService'; +import ArrowLeft from '../assets/arrow-left.svg'; +import caretSort from '../assets/caret-sort.svg'; +import Edit from '../assets/edit.svg'; +import NoFilesDarkIcon from '../assets/no-files-dark.svg'; +import NoFilesIcon from '../assets/no-files.svg'; +import SyncIcon from '../assets/sync.svg'; +import Trash from '../assets/trash.svg'; +import Pagination from '../components/DocumentPagination'; +import DropdownMenu from '../components/DropdownMenu'; +import Input from '../components/Input'; +import SkeletonLoader from '../components/SkeletonLoader'; +import Spinner from '../components/Spinner'; +import { useDarkTheme, useLoaderState } from '../hooks'; +import ChunkModal from '../modals/ChunkModal'; +import ConfirmationModal from '../modals/ConfirmationModal'; +import { ActiveState, Doc, DocumentsProps } from '../models/misc'; +import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi'; +import { + setPaginatedDocuments, + setSourceDocs, +} from '../preferences/preferenceSlice'; +import Upload from '../upload/Upload'; +import { formatDate } from '../utils/dateTimeUtils'; +import { ChunkType } from './types'; + const formatTokens = (tokens: number): string => { const roundToTwoDecimals = (num: number): string => { return (Math.round((num + Number.EPSILON) * 100) / 100).toString(); @@ -33,76 +44,83 @@ const formatTokens = (tokens: number): string => { } }; -const Documents: React.FC = ({ +export default function Documents({ paginatedDocuments, handleDeleteDocument, -}) => { +}: DocumentsProps) { const { t } = useTranslation(); const dispatch = useDispatch(); - // State for search input - const [searchTerm, setSearchTerm] = useState(''); - // State for modal: active/inactive - const [modalState, setModalState] = useState('INACTIVE'); // Initialize with inactive state - const [isOnboarding, setIsOnboarding] = useState(false); // State for onboarding flag - const [loading, setLoading] = useState(false); + + const [searchTerm, setSearchTerm] = useState(''); + const [modalState, setModalState] = useState('INACTIVE'); + const [isOnboarding, setIsOnboarding] = useState(false); + const [loading, setLoading] = useLoaderState(false); const [sortField, setSortField] = useState<'date' | 'tokens'>('date'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); // Pagination const [currentPage, setCurrentPage] = useState(1); const [rowsPerPage, setRowsPerPage] = useState(10); const [totalPages, setTotalPages] = useState(1); - // const [totalDocuments, setTotalDocuments] = useState(0); - // Filter documents based on the search term - const filteredDocuments = paginatedDocuments?.filter((document) => - document.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); - // State for documents - const currentDocuments = filteredDocuments ?? []; - console.log('currentDocuments', currentDocuments); + const currentDocuments = paginatedDocuments ?? []; const syncOptions = [ - { label: 'Never', value: 'never' }, - { label: 'Daily', value: 'daily' }, - { label: 'Weekly', value: 'weekly' }, - { label: 'Monthly', value: 'monthly' }, + { label: t('settings.documents.syncFrequency.never'), value: 'never' }, + { label: t('settings.documents.syncFrequency.daily'), value: 'daily' }, + { label: t('settings.documents.syncFrequency.weekly'), value: 'weekly' }, + { label: t('settings.documents.syncFrequency.monthly'), value: 'monthly' }, ]; + const [showDocumentChunks, setShowDocumentChunks] = useState(); - const refreshDocs = ( - field: 'date' | 'tokens' | undefined, - pageNumber?: number, - rows?: number, - ) => { - const page = pageNumber ?? currentPage; - const rowsPerPg = rows ?? rowsPerPage; + const refreshDocs = useCallback( + ( + field: 'date' | 'tokens' | undefined, + pageNumber?: number, + rows?: number, + ) => { + const page = pageNumber ?? currentPage; + const rowsPerPg = rows ?? rowsPerPage; - if (field !== undefined) { - if (field === sortField) { - // Toggle sort order - setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); - } else { - // Change sort field and reset order to 'desc' - setSortField(field); - setSortOrder('desc'); + // If field is undefined, (Pagination or Search) use the current sortField + const newSortField = field ?? sortField; + + // If field is undefined, (Pagination or Search) use the current sortOrder + const newSortOrder = + field === sortField + ? sortOrder === 'asc' + ? 'desc' + : 'asc' + : sortOrder; + + // If field is defined, update the sortField and sortOrder + if (field) { + setSortField(newSortField); + setSortOrder(newSortOrder); } - } - getDocsWithPagination(sortField, sortOrder, page, rowsPerPg) - .then((data) => { - //dispatch(setSourceDocs(data ? data.docs : [])); - dispatch(setPaginatedDocuments(data ? data.docs : [])); - setTotalPages(data ? data.totalPages : 0); - //setTotalDocuments(data ? data.totalDocuments : 0); - }) - .catch((error) => console.error(error)) - .finally(() => { - setLoading(false); - }); - }; + + setLoading(true); + getDocsWithPagination( + newSortField, + newSortOrder, + page, + rowsPerPg, + searchTerm, + ) + .then((data) => { + dispatch(setPaginatedDocuments(data ? data.docs : [])); + setTotalPages(data ? data.totalPages : 0); + }) + .catch((error) => console.error(error)) + .finally(() => { + setLoading(false); + }); + }, + [currentPage, rowsPerPage, sortField, sortOrder, searchTerm], + ); const handleManageSync = (doc: Doc, sync_frequency: string) => { setLoading(true); userService .manageSync({ source_id: doc.id, sync_frequency }) .then(() => { - // First, fetch the updated source docs return getDocs(); }) .then((data) => { @@ -126,169 +144,492 @@ const Documents: React.FC = ({ }); }; - useEffect(() => { - if (modalState === 'INACTIVE') { - refreshDocs(sortField, currentPage, rowsPerPage); - } - }, [modalState, sortField, currentPage, rowsPerPage]); + const [documentToDelete, setDocumentToDelete] = useState<{ + index: number; + document: Doc; + } | null>(null); + const [deleteModalState, setDeleteModalState] = + useState('INACTIVE'); - return ( -
-
-
-
-
- setSearchTerm(e.target.value)} // Handle search input change - /> -
- + borderVariant="thin" + />
- {loading ? ( - - ) : ( - + + + +
+ {' '} +
+
- - - + + - - - - - {!currentDocuments?.length && ( + + {loading ? ( + + ) : !currentDocuments?.length ? ( - - )} - {Array.isArray(currentDocuments) && + ) : ( currentDocuments.map((document, index) => ( - - - - setShowDocumentChunks(document)} + > + + + - - - ))} + )) + )}
{t('settings.documents.name')} +
+ {t('settings.documents.name')} +
{t('settings.documents.date')} refreshDocs('date')} src={caretSort} alt="sort" />
+
- {t('settings.documents.tokenUsage')} + + {t('settings.documents.tokenUsage')} + + + {t('settings.documents.tokenUsage')} + refreshDocs('tokens')} src={caretSort} alt="sort" />
-
- {t('settings.documents.type')} -
+
+ + {t('settings.documents.actions')} +
+ {t('settings.documents.noData')}
{document.name}{document.date} +
+ {document.name} + + {document.date ? formatDate(document.date) : ''} + {document.tokens ? formatTokens(+document.tokens) : ''} - {document.type === 'remote' ? 'Pre-loaded' : 'Private'} - -
- {document.type !== 'remote' && ( - Delete { - event.stopPropagation(); - handleDeleteDocument(index, document); - }} - /> +
+
+ {!document.syncFrequency && ( +
)} {document.syncFrequency && ( -
- { - handleManageSync(document, value); - }} - defaultValue={document.syncFrequency} - icon={SyncIcon} - /> -
+ { + handleManageSync(document, value); + }} + defaultValue={document.syncFrequency} + icon={SyncIcon} + /> )} +
+
+
+
+ +
+ { + setCurrentPage(page); + refreshDocs(undefined, page, rowsPerPage); + }} + onRowsPerPageChange={(rows) => { + setRowsPerPage(rows); + setCurrentPage(1); + refreshDocs(undefined, 1, rows); + }} + /> +
+ + {modalState === 'ACTIVE' && ( + setModalState('INACTIVE')} + onSuccessfulUpload={() => + refreshDocs(undefined, currentPage, rowsPerPage) + } + /> + )} + + {deleteModalState === 'ACTIVE' && documentToDelete && ( + { + setDeleteModalState('INACTIVE'); + setDocumentToDelete(null); + }} + submitLabel={t('convTile.delete')} + /> + )} +
+ ); +} + +function DocumentChunks({ + document, + handleGoBack, +}: { + document: Doc; + handleGoBack: () => void; +}) { + const { t } = useTranslation(); + const [isDarkTheme] = useDarkTheme(); + const [paginatedChunks, setPaginatedChunks] = useState([]); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(5); + const [totalChunks, setTotalChunks] = useState(0); + const [loading, setLoading] = useLoaderState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [addModal, setAddModal] = useState('INACTIVE'); + const [editModal, setEditModal] = useState<{ + state: ActiveState; + chunk: ChunkType | null; + }>({ state: 'INACTIVE', chunk: null }); + + const fetchChunks = () => { + setLoading(true); + try { + userService + .getDocumentChunks(document.id ?? '', page, perPage) + .then((response) => { + if (!response.ok) { + setLoading(false); + setPaginatedChunks([]); + throw new Error('Failed to fetch chunks data'); + } + return response.json(); + }) + .then((data) => { + setPage(data.page); + setPerPage(data.per_page); + setTotalChunks(data.total); + setPaginatedChunks(data.chunks); + setLoading(false); + }); + } catch (e) { + console.log(e); + setLoading(false); + } + }; + + const handleAddChunk = (title: string, text: string) => { + try { + userService + .addChunk({ + id: document.id ?? '', + text: text, + metadata: { + title: title, + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to add chunk'); + } + fetchChunks(); + }); + } catch (e) { + console.log(e); + } + }; + + const handleUpdateChunk = (title: string, text: string, chunk: ChunkType) => { + try { + userService + .updateChunk({ + id: document.id ?? '', + chunk_id: chunk.doc_id, + text: text, + metadata: { + title: title, + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to update chunk'); + } + fetchChunks(); + }); + } catch (e) { + console.log(e); + } + }; + + const handleDeleteChunk = (chunk: ChunkType) => { + try { + userService + .deleteChunk(document.id ?? '', chunk.doc_id) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to delete chunk'); + } + setEditModal({ state: 'INACTIVE', chunk: null }); + fetchChunks(); + }); + } catch (e) { + console.log(e); + } + }; + + React.useEffect(() => { + fetchChunks(); + }, [page, perPage]); + return ( +
+
+ +

Back to all documents

+
+
+
+

{`${totalChunks} Chunks`}

+ + { + setSearchTerm(e.target.value); + }} + borderVariant="thin" + /> +
+ +
+ {loading ? ( +
+
+ +
+
+ ) : ( +
+ {paginatedChunks.filter((chunk) => + chunk.metadata?.title + .toLowerCase() + .includes(searchTerm.toLowerCase()), + ).length === 0 ? ( +
+ No tools found + No chunks found +
+ ) : ( + paginatedChunks + .filter((chunk) => + chunk.metadata?.title + .toLowerCase() + .includes(searchTerm.toLowerCase()), + ) + .map((chunk, index) => ( +
+
+
+ +
+
+

+ {chunk.metadata?.title} +

+

+ {chunk.text} +

+
+
+
+ )) )}
- {/* Conditionally render the Upload modal based on modalState */} - {modalState === 'ACTIVE' && ( -
-
- {/* Your Upload component */} - -
+ )} + {!loading && + paginatedChunks.filter((chunk) => + chunk.metadata?.title + .toLowerCase() + .includes(searchTerm.toLowerCase()), + ).length !== 0 && ( +
+ { + setPage(page); + }} + onRowsPerPageChange={(rows) => { + setPerPage(rows); + setPage(1); + }} + />
)} -
- {/* Pagination component with props: - # Note: Every time the page changes, - the refreshDocs function is called with the updated page number and rows per page. - and reset cursor paginated query parameter to undefined. - */} - { - setCurrentPage(page); - refreshDocs(sortField, page, rowsPerPage); + + setEditModal((prev) => ({ ...prev, state }))} + handleSubmit={(title, text) => { + handleUpdateChunk(title, text, editModal.chunk as ChunkType); }} - onRowsPerPageChange={(rows) => { - setRowsPerPage(rows); - setCurrentPage(1); - refreshDocs(sortField, 1, rows); + originalText={editModal.chunk?.text} + originalTitle={editModal.chunk?.metadata?.title} + handleDelete={() => { + handleDeleteChunk(editModal.chunk as ChunkType); }} />
); -}; - -Documents.propTypes = { - //documents: PropTypes.array.isRequired, - handleDeleteDocument: PropTypes.func.isRequired, -}; - -export default Documents; +} diff --git a/frontend/src/settings/General.tsx b/frontend/src/settings/General.tsx index e0a24a75..d974eab7 100644 --- a/frontend/src/settings/General.tsx +++ b/frontend/src/settings/General.tsx @@ -19,27 +19,20 @@ import Prompts from './Prompts'; export default function General() { const { t, - i18n: { changeLanguage, language }, + i18n: { changeLanguage }, } = useTranslation(); - const themes = ['Light', 'Dark']; + const themes = [ + { value: 'Light', label: t('settings.general.light') }, + { value: 'Dark', label: t('settings.general.dark') }, + ]; const languageOptions = [ - { - label: 'English', - value: 'en', - }, - { - label: 'Spanish', - value: 'es', - }, - { - label: 'Japanese', - value: 'jp', - }, - { - label: 'Mandarin', - value: 'zh', - }, + { label: 'English', value: 'en' }, + { label: 'Español', value: 'es' }, + { label: '日本語', value: 'jp' }, + { label: '普通话', value: 'zh' }, + { label: '繁體中文(臺灣)', value: 'zhTW' }, + { label: 'Русский', value: 'ru' }, ]; const chunks = ['0', '2', '4', '6', '8', '10']; const token_limits = new Map([ @@ -91,15 +84,17 @@ export default function General() { return (
-

+

+ { - setSelectedTheme(option); - option !== selectedTheme && toggleTheme(); + selectedValue={ + themes.find((theme) => theme.value === selectedTheme) || null + } + onSelect={(option: { value: string; label: string }) => { + setSelectedTheme(option.value); + option.value !== selectedTheme && toggleTheme(); }} size="w-56" rounded="3xl" @@ -107,9 +102,9 @@ export default function General() { />
-

+

+ @@ -125,9 +120,9 @@ export default function General() { />
-

+

+
-

+

+ ({ value: value, @@ -173,16 +168,14 @@ export default function General() { />
-

+

+
diff --git a/frontend/src/settings/Logs.tsx b/frontend/src/settings/Logs.tsx index 1e248d46..3f4d725b 100644 --- a/frontend/src/settings/Logs.tsx +++ b/frontend/src/settings/Logs.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import userService from '../api/services/userService'; import ChevronRight from '../assets/chevron-right.svg'; @@ -6,15 +7,17 @@ import Dropdown from '../components/Dropdown'; import SkeletonLoader from '../components/SkeletonLoader'; import { APIKeyData, LogData } from './types'; import CoppyButton from '../components/CopyButton'; +import { useLoaderState } from '../hooks'; export default function Logs() { + const { t } = useTranslation(); const [chatbots, setChatbots] = useState([]); const [selectedChatbot, setSelectedChatbot] = useState(); const [logs, setLogs] = useState([]); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); - const [loadingChatbots, setLoadingChatbots] = useState(true); - const [loadingLogs, setLoadingLogs] = useState(true); + const [loadingChatbots, setLoadingChatbots] = useLoaderState(true); + const [loadingLogs, setLoadingLogs] = useLoaderState(true); const fetchChatbots = async () => { setLoadingChatbots(true); @@ -64,13 +67,16 @@ export default function Logs() { return (
-
-

- Filter by chatbot -

- {loadingChatbots ? ( - - ) : ( + {loadingChatbots ? ( + + ) : ( +
+ { setSelectedChatbot( chatbots.find((item) => item.id === chatbot.value), @@ -99,16 +105,12 @@ export default function Logs() { rounded="3xl" border="border" /> - )} -
+
+ )}
- {loadingLogs ? ( - - ) : ( - - )} +
); @@ -117,9 +119,11 @@ export default function Logs() { type LogsTableProps = { logs: LogData[]; setPage: React.Dispatch>; + loading: boolean; }; -function LogsTable({ logs, setPage }: LogsTableProps) { +function LogsTable({ logs, setPage, loading }: LogsTableProps) { + const { t } = useTranslation(); const observerRef = useRef(); const firstObserver = useCallback((node: HTMLDivElement) => { if (observerRef.current) { @@ -134,14 +138,14 @@ function LogsTable({ logs, setPage }: LogsTableProps) {

- API generated / chatbot conversations + {t('settings.logs.tableHeader')}

- {logs.map((log, index) => { + {logs?.map((log, index) => { if (index === logs.length - 1) { return (
@@ -150,12 +154,14 @@ function LogsTable({ logs, setPage }: LogsTableProps) { ); } else return ; })} + {loading && }
); } function Log({ log }: { log: LogData }) { + const { t } = useTranslation(); const logLevelColor = { info: 'text-green-500', error: 'text-red-500', @@ -167,7 +173,7 @@ function Log({ log }: { log: LogData }) { chevron-right diff --git a/frontend/src/settings/Prompts.tsx b/frontend/src/settings/Prompts.tsx index 6e1810e5..611b0b90 100644 --- a/frontend/src/settings/Prompts.tsx +++ b/frontend/src/settings/Prompts.tsx @@ -168,7 +168,7 @@ export default function Prompts({ />
+

Back to all tools

+
+
+

+ Type +

+

+ {tool.name} +

+
+
+ {Object.keys(tool?.config).length !== 0 && tool.name !== 'api_tool' && ( +

+ Authentication +

+ )} +
+ {Object.keys(tool?.config).length !== 0 && + tool.name !== 'api_tool' && ( +
+ + API Key / Oauth + + setAuthKey(e.target.value)} + borderVariant="thin" + placeholder="Enter API Key / Oauth" + > +
+ )} +
+ + +
+
+
+
+
+
+

+ Actions +

+ +
+ {tool.name === 'api_tool' ? ( + + ) : ( +
+ {'actions' in tool && + tool.actions.map((action, actionIndex) => { + return ( +
+
+

+ {action.name} +

+ +
+
+ { + setTool({ + ...tool, + actions: tool.actions.map((act, index) => { + if (index === actionIndex) { + return { + ...act, + description: e.target.value, + }; + } + return act; + }), + }); + }} + borderVariant="thin" + > +
+
+ + + + + + + + + + + + {Object.entries(action.parameters?.properties).map( + (param, index) => { + const uniqueKey = `${actionIndex}-${param[0]}`; + return ( + + + + + + + + ); + }, + )} + +
Field NameField TypeFilled by LLMFIeld descriptionValue
{param[0]}{param[1].type} + + + { + setTool({ + ...tool, + actions: tool.actions.map( + (act, index) => { + if (index === actionIndex) { + return { + ...act, + parameters: { + ...act.parameters, + properties: { + ...act.parameters + .properties, + [param[0]]: { + ...act.parameters + .properties[param[0]], + description: + e.target.value, + }, + }, + }, + }; + } + return act; + }, + ), + }); + }} + > + + { + setTool({ + ...tool, + actions: tool.actions.map( + (act, index) => { + if (index === actionIndex) { + return { + ...act, + parameters: { + ...act.parameters, + properties: { + ...act.parameters + .properties, + [param[0]]: { + ...act.parameters + .properties[param[0]], + value: e.target.value, + }, + }, + }, + }; + } + return act; + }, + ), + }); + }} + > +
+
+
+ ); + })} +
+ )} + +
+
+ ); +} + +function APIToolConfig({ + tool, + setTool, +}: { + tool: APIToolType; + setTool: (tool: APIToolType) => void; +}) { + const [apiTool, setApiTool] = React.useState(tool); + + const handleActionChange = ( + actionName: string, + updatedAction: APIActionType, + ) => { + setApiTool((prevApiTool) => { + const updatedActions = { ...prevApiTool.config.actions }; + updatedActions[actionName] = updatedAction; + return { + ...prevApiTool, + config: { ...prevApiTool.config, actions: updatedActions }, + }; + }); + }; + + const handleActionToggle = (actionName: string) => { + setApiTool((prevApiTool) => { + const updatedActions = { ...prevApiTool.config.actions }; + const updatedAction = { ...updatedActions[actionName] }; + updatedAction.active = !updatedAction.active; + updatedActions[actionName] = updatedAction; + return { + ...prevApiTool, + config: { ...prevApiTool.config, actions: updatedActions }, + }; + }); + }; + + React.useEffect(() => { + setApiTool(tool); + }, [tool]); + + React.useEffect(() => { + setTool(apiTool); + }, [apiTool]); + return ( +
+ {apiTool.config.actions && + Object.entries(apiTool.config.actions).map( + ([actionName, action], actionIndex) => { + return ( +
+
+

+ {action.name} +

+ +
+
+
+ + URL + + { + setApiTool((prevApiTool) => { + const updatedActions = { + ...prevApiTool.config.actions, + }; + const updatedAction = { + ...updatedActions[actionName], + }; + updatedAction.url = e.target.value; + updatedActions[actionName] = updatedAction; + return { + ...prevApiTool, + config: { + ...prevApiTool.config, + actions: updatedActions, + }, + }; + }); + }} + borderVariant="thin" + placeholder="Enter url" + > +
+
+
+
+ + Method + + { + setApiTool((prevApiTool) => { + const updatedActions = { + ...prevApiTool.config.actions, + }; + const updatedAction = { + ...updatedActions[actionName], + }; + updatedAction.method = value as + | 'GET' + | 'POST' + | 'PUT' + | 'DELETE'; + updatedActions[actionName] = updatedAction; + return { + ...prevApiTool, + config: { + ...prevApiTool.config, + actions: updatedActions, + }, + }; + }); + }} + size="w-56" + rounded="3xl" + border="border" + /> +
+
+
+
+ + Description + + { + setApiTool((prevApiTool) => { + const updatedActions = { + ...prevApiTool.config.actions, + }; + const updatedAction = { + ...updatedActions[actionName], + }; + updatedAction.description = e.target.value; + updatedActions[actionName] = updatedAction; + return { + ...prevApiTool, + config: { + ...prevApiTool.config, + actions: updatedActions, + }, + }; + }); + }} + borderVariant="thin" + placeholder="Enter description" + > +
+
+
+ +
+
+ ); + }, + )} +
+ ); +} + +function APIActionTable({ + apiAction, + handleActionChange, +}: { + apiAction: APIActionType; + handleActionChange: ( + actionName: string, + updatedAction: APIActionType, + ) => void; +}) { + const [action, setAction] = React.useState(apiAction); + const [newPropertyKey, setNewPropertyKey] = React.useState(''); + const [addingPropertySection, setAddingPropertySection] = React.useState< + 'headers' | 'query_params' | 'body' | null + >(null); + const [editingPropertyKey, setEditingPropertyKey] = React.useState<{ + section: 'headers' | 'query_params' | 'body' | null; + oldKey: string | null; + }>({ section: null, oldKey: null }); + + const handlePropertyChange = ( + section: 'headers' | 'query_params' | 'body', + key: string, + field: 'value' | 'description' | 'filled_by_llm', + value: string | number | boolean, + ) => { + setAction((prevAction) => { + const updatedProperties = { + ...prevAction[section].properties, + [key]: { + ...prevAction[section].properties[key], + [field]: value, + }, + }; + return { + ...prevAction, + [section]: { + ...prevAction[section], + properties: updatedProperties, + }, + }; + }); + }; + + const handleAddPropertyStart = ( + section: 'headers' | 'query_params' | 'body', + ) => { + setEditingPropertyKey({ section: null, oldKey: null }); + setAddingPropertySection(section); + setNewPropertyKey(''); + }; + const handleAddPropertyCancel = () => { + setAddingPropertySection(null); + setNewPropertyKey(''); + }; + const handleAddProperty = () => { + if (addingPropertySection && newPropertyKey.trim() !== '') { + setAction((prevAction) => { + const updatedProperties = { + ...prevAction[addingPropertySection].properties, + [newPropertyKey.trim()]: { + type: 'string', + description: '', + value: '', + filled_by_llm: false, + }, + }; + return { + ...prevAction, + [addingPropertySection]: { + ...prevAction[addingPropertySection], + properties: updatedProperties, + }, + }; + }); + setNewPropertyKey(''); + setAddingPropertySection(null); + } + }; + + const handleRenamePropertyStart = ( + section: 'headers' | 'query_params' | 'body', + oldKey: string, + ) => { + setAddingPropertySection(null); + setEditingPropertyKey({ section, oldKey }); + setNewPropertyKey(oldKey); + }; + const handleRenamePropertyCancel = () => { + setEditingPropertyKey({ section: null, oldKey: null }); + setNewPropertyKey(''); + }; + const handleRenameProperty = () => { + if ( + editingPropertyKey.section && + editingPropertyKey.oldKey && + newPropertyKey.trim() !== '' && + newPropertyKey.trim() !== editingPropertyKey.oldKey + ) { + setAction((prevAction) => { + const { section, oldKey } = editingPropertyKey; + if (section && oldKey) { + const { [oldKey]: oldProperty, ...restProperties } = + prevAction[section].properties; + const updatedProperties = { + ...restProperties, + [newPropertyKey.trim()]: oldProperty, + }; + return { + ...prevAction, + [section]: { + ...prevAction[section], + properties: updatedProperties, + }, + }; + } + return prevAction; + }); + setEditingPropertyKey({ section: null, oldKey: null }); + setNewPropertyKey(''); + } + }; + + const handlePorpertyDelete = ( + section: 'headers' | 'query_params' | 'body', + key: string, + ) => { + setAction((prevAction) => { + const { [key]: deletedProperty, ...restProperties } = + prevAction[section].properties; + return { + ...prevAction, + [section]: { + ...prevAction[section], + properties: restProperties, + }, + }; + }); + }; + + React.useEffect(() => { + setAction(apiAction); + }, [apiAction]); + + React.useEffect(() => { + handleActionChange(action.name, action); + }, [action]); + const renderPropertiesTable = ( + section: 'headers' | 'query_params' | 'body', + ) => { + return ( + <> + {Object.entries(action[section].properties).map( + ([key, param], index) => ( + + + {editingPropertyKey.section === section && + editingPropertyKey.oldKey === key ? ( +
+ setNewPropertyKey(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleRenameProperty(); + } + }} + /> +
+ + +
+
+ ) : ( + handleRenamePropertyStart(section, key)} + readOnly + /> + )} + + {param.type} + + + + + + handlePropertyChange( + section, + key, + 'description', + e.target.value, + ) + } + > + + + + handlePropertyChange(section, key, 'value', e.target.value) + } + className={`bg-transparent border border-silver dark:border-silver/40 outline-none px-2 py-1 rounded-lg text-sm ${param.filled_by_llm ? 'opacity-50' : ''}`} + > + + + + + + ), + )} + {addingPropertySection === section ? ( + + + setNewPropertyKey(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleAddProperty(); + } + }} + placeholder="New property key" + className="min-w-[130.5px] w-full flex items-start bg-transparent border border-silver dark:border-silver/40 outline-none px-2 py-1 rounded-lg text-sm" + /> + + + + + + + + ) : ( + + + + + + + )} + + ); + }; + return ( +
+
+

+ Headers +

+ + + + + + + + + + + + {renderPropertiesTable('headers')} +
NameTypeFilled by LLMDescriptionValue
+
+
+

+ Query Parameters +

+ + + + + + + + + + + + {renderPropertiesTable('query_params')} +
NameTypeFilled by LLMDescriptionValue
+
+
+

+ Body +

+ + + + + + + + + + + + {renderPropertiesTable('body')} +
NameTypeFilled by LLMDescriptionValue
+
+
+ ); +} diff --git a/frontend/src/settings/Tools.tsx b/frontend/src/settings/Tools.tsx new file mode 100644 index 00000000..0d45b9ba --- /dev/null +++ b/frontend/src/settings/Tools.tsx @@ -0,0 +1,212 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import userService from '../api/services/userService'; +import CogwheelIcon from '../assets/cogwheel.svg'; +import NoFilesDarkIcon from '../assets/no-files-dark.svg'; +import NoFilesIcon from '../assets/no-files.svg'; +import Input from '../components/Input'; +import { useDarkTheme } from '../hooks'; +import AddToolModal from '../modals/AddToolModal'; +import { ActiveState } from '../models/misc'; +import ToolConfig from './ToolConfig'; +import { APIToolType, UserToolType } from './types'; + +export default function Tools() { + const { t } = useTranslation(); + const [isDarkTheme] = useDarkTheme(); + const [searchTerm, setSearchTerm] = React.useState(''); + const [addToolModalState, setAddToolModalState] = + React.useState('INACTIVE'); + const [userTools, setUserTools] = React.useState([]); + const [selectedTool, setSelectedTool] = React.useState< + UserToolType | APIToolType | null + >(null); + + const getUserTools = () => { + userService + .getUserTools() + .then((res) => { + return res.json(); + }) + .then((data) => { + setUserTools(data.tools); + }); + }; + + const updateToolStatus = (toolId: string, newStatus: boolean) => { + userService + .updateToolStatus({ id: toolId, status: newStatus }) + .then(() => { + setUserTools((prevTools) => + prevTools.map((tool) => + tool.id === toolId ? { ...tool, status: newStatus } : tool, + ), + ); + }) + .catch((error) => { + console.error('Failed to update tool status:', error); + }); + }; + + const handleSettingsClick = (tool: UserToolType) => { + setSelectedTool(tool); + }; + + const handleGoBack = () => { + setSelectedTool(null); + getUserTools(); + }; + + const handleToolAdded = (toolId: string) => { + userService + .getUserTools() + .then((res) => res.json()) + .then((data) => { + const newTool = data.tools.find( + (tool: UserToolType) => tool.id === toolId, + ); + if (newTool) { + setSelectedTool(newTool); + } else { + console.error('Newly added tool not found'); + } + }) + .catch((error) => console.error('Error fetching tools:', error)); + }; + + React.useEffect(() => { + getUserTools(); + }, []); + + return ( +
+ {selectedTool ? ( + + ) : ( +
+
+
+
+ + setSearchTerm(e.target.value)} + borderVariant="thin" + /> +
+ +
+
+ {userTools.filter((tool) => + tool.displayName + .toLowerCase() + .includes(searchTerm.toLowerCase()), + ).length === 0 ? ( +
+ No tools found + {t('settings.tools.noToolsFound')} +
+ ) : ( + userTools + .filter((tool) => + tool.displayName + .toLowerCase() + .includes(searchTerm.toLowerCase()), + ) + .map((tool, index) => ( +
+
+
+ {`${tool.displayName} + +
+
+

+ {tool.displayName} +

+

+ {tool.description} +

+
+
+
+ +
+
+ )) + )} +
+
+ +
+ )} +
+ ); +} diff --git a/frontend/src/settings/index.tsx b/frontend/src/settings/index.tsx index 15c7ce08..918e4d15 100644 --- a/frontend/src/settings/index.tsx +++ b/frontend/src/settings/index.tsx @@ -7,8 +7,8 @@ import SettingsBar from '../components/SettingsBar'; import i18n from '../locale/i18n'; import { Doc } from '../models/misc'; import { - selectSourceDocs, selectPaginatedDocuments, + selectSourceDocs, setPaginatedDocuments, setSourceDocs, } from '../preferences/preferenceSlice'; @@ -17,6 +17,7 @@ import APIKeys from './APIKeys'; import Documents from './Documents'; import General from './General'; import Logs from './Logs'; +import Tools from './Tools'; import Widgets from './Widgets'; export default function Settings() { @@ -90,8 +91,8 @@ export default function Settings() { case 'Widgets': return ( ); case t('settings.apiKeys.label'): @@ -100,6 +101,8 @@ export default function Settings() { return ; case t('settings.logs.label'): return ; + case t('settings.tools.label'): + return ; default: return null; } diff --git a/frontend/src/settings/types/index.ts b/frontend/src/settings/types/index.ts index 52a58f23..0795fbb0 100644 --- a/frontend/src/settings/types/index.ts +++ b/frontend/src/settings/types/index.ts @@ -1,3 +1,9 @@ +export type ChunkType = { + doc_id: string; + text: string; + metadata: { [key: string]: string }; +}; + export type APIKeyData = { id: string; name: string; @@ -18,3 +24,64 @@ export type LogData = { retriever_params: Record; timestamp: string; }; + +export type ParameterGroupType = { + type: 'object'; + properties: { + [key: string]: { + type: 'string' | 'integer'; + description: string; + value: string | number; + filled_by_llm: boolean; + }; + }; +}; + +export type UserToolType = { + id: string; + name: string; + displayName: string; + description: string; + status: boolean; + config: { + [key: string]: string; + }; + actions: { + name: string; + description: string; + parameters: { + properties: { + [key: string]: { + type: string; + description: string; + filled_by_llm: boolean; + value: string; + }; + }; + additionalProperties: boolean; + required: string[]; + type: string; + }; + active: boolean; + }[]; +}; + +export type APIActionType = { + name: string; + url: string; + description: string; + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + query_params: ParameterGroupType; + headers: ParameterGroupType; + body: ParameterGroupType; + active: boolean; +}; + +export type APIToolType = { + id: string; + name: string; + displayName: string; + description: string; + status: boolean; + config: { actions: { [key: string]: APIActionType } }; +}; diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index 2f28042a..248d9f11 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -4,12 +4,11 @@ import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import userService from '../api/services/userService'; -import Exit from '../assets/exit.svg'; -import ArrowLeft from '../assets/arrow-left.svg'; import FileUpload from '../assets/file_upload.svg'; import WebsiteCollect from '../assets/website_collect.svg'; import Dropdown from '../components/Dropdown'; import Input from '../components/Input'; +import ToggleSwitch from '../components/ToggleSwitch'; import { ActiveState, Doc } from '../models/misc'; import { getDocs } from '../preferences/preferenceApi'; import { @@ -17,29 +16,174 @@ import { setSourceDocs, selectSourceDocs, } from '../preferences/preferenceSlice'; +import WrapperModal from '../modals/WrapperModal'; +import { + IngestorType, + IngestorConfig, + IngestorFormSchemas, + FormField, +} from './types/ingestor'; +import { IngestorDefaultConfigs } from '../upload/types/ingestor'; function Upload({ - modalState, + receivedFile = [], setModalState, isOnboarding, + renderTab = null, + close, + onSuccessfulUpload = () => undefined, }: { - modalState: ActiveState; + receivedFile: File[]; setModalState: (state: ActiveState) => void; isOnboarding: boolean; + renderTab: string | null; + close: () => void; + onSuccessfulUpload?: () => void; }) { - const [docName, setDocName] = useState(''); - const [urlName, setUrlName] = useState(''); - const [url, setUrl] = useState(''); - const [repoUrl, setRepoUrl] = useState(''); // P3f93 - const [redditData, setRedditData] = useState({ - client_id: '', - client_secret: '', - user_agent: '', - search_queries: [''], - number_posts: 10, + const [docName, setDocName] = useState(receivedFile[0]?.name); + const [remoteName, setRemoteName] = useState(''); + const [files, setfiles] = useState(receivedFile); + const [activeTab, setActiveTab] = useState(renderTab); + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + + const renderFormFields = () => { + const schema = IngestorFormSchemas[ingestor.type]; + if (!schema) return null; + + const generalFields = schema.filter((field) => !field.advanced); + const advancedFields = schema.filter((field) => field.advanced); + + return ( +
+
+ {generalFields.map((field: FormField) => renderField(field))} +
+ + {advancedFields.length > 0 && ( +
+
+
+
+ {advancedFields.map((field: FormField) => renderField(field))} +
+
+
+ )} +
+ ); + }; + + const renderField = (field: FormField) => { + const isRequired = field.required ?? false; + switch (field.type) { + case 'string': + return ( + + handleIngestorChange( + field.name as keyof IngestorConfig['config'], + e.target.value, + ) + } + borderVariant="thin" + label={field.label} + required={isRequired} + colorVariant="gray" + /> + ); + case 'number': + return ( + + handleIngestorChange( + field.name as keyof IngestorConfig['config'], + Number(e.target.value), + ) + } + borderVariant="thin" + label={field.label} + required={isRequired} + colorVariant="gray" + /> + ); + case 'enum': + return ( + + opt.value === + ingestor.config[field.name as keyof typeof ingestor.config], + ) || null + } + onSelect={(selected: { label: string; value: string }) => { + handleIngestorChange( + field.name as keyof IngestorConfig['config'], + selected.value, + ); + }} + size="w-full" + rounded="3xl" + placeholder={field.label} + border="border" + borderColor="gray-5000" + /> + ); + case 'boolean': + return ( + { + handleIngestorChange( + field.name as keyof IngestorConfig['config'], + checked, + ); + }} + className="mt-2" + /> + ); + default: + return null; + } + }; + + // New unified ingestor state + const [ingestor, setIngestor] = useState(() => { + const defaultType: IngestorType = 'crawler'; + const defaultConfig = IngestorDefaultConfigs[defaultType]; + return { + type: defaultType, + name: defaultConfig.name, + config: defaultConfig.config, + }; }); - const [activeTab, setActiveTab] = useState(null); - const [files, setfiles] = useState([]); + const [progress, setProgress] = useState<{ type: 'UPLOAD' | 'TRAINING'; percentage: number; @@ -50,19 +194,13 @@ function Upload({ const { t } = useTranslation(); const setTimeoutRef = useRef(); - const urlOptions: { label: string; value: string }[] = [ + const urlOptions: { label: string; value: IngestorType }[] = [ { label: 'Crawler', value: 'crawler' }, - // { label: 'Sitemap', value: 'sitemap' }, { label: 'Link', value: 'url' }, + { label: 'GitHub', value: 'github' }, { label: 'Reddit', value: 'reddit' }, - { label: 'GitHub', value: 'github' }, // P3f93 ]; - const [urlType, setUrlType] = useState<{ label: string; value: string }>({ - label: 'Crawler', - value: 'crawler', - }); - const sourceDocs = useSelector(selectSourceDocs); useEffect(() => { if (setTimeoutRef.current) { @@ -110,12 +248,14 @@ function Upload({

{isTraining && - (progress?.percentage === 100 ? 'Training completed' : title)} + (progress?.percentage === 100 + ? t('modals.uploadDoc.progress.completed') + : title)} {!isTraining && title}

-

This may take several minutes

+

{t('modals.uploadDoc.progress.wait')}

- Over the token limit, please consider uploading smaller document + {t('modals.uploadDoc.progress.tokenLimit')}

{/*

{progress?.percentage || 0}%

*/} @@ -145,7 +285,7 @@ function Upload({ } function UploadProgress() { - return ; + return ; } function TrainingProgress() { @@ -213,6 +353,7 @@ function Upload({ setfiles([]); setProgress(undefined); setModalState('INACTIVE'); + onSuccessfulUpload?.(); } } else if (data.status == 'PROGRESS') { setProgress( @@ -236,7 +377,7 @@ function Upload({ }, [progress, dispatch]); return ( { setfiles(acceptedFiles); - setDocName(acceptedFiles[0]?.name); + setDocName(acceptedFiles[0]?.name || ''); }, []); const doNothing = () => undefined; @@ -256,6 +397,7 @@ function Upload({ files.forEach((file) => { formData.append('file', file); }); + formData.append('name', docName); formData.append('user', 'local'); const apiHost = import.meta.env.VITE_API_HOST; @@ -276,38 +418,58 @@ function Upload({ const uploadRemote = () => { const formData = new FormData(); - formData.append('name', urlName); + formData.append('name', remoteName); formData.append('user', 'local'); - if (urlType !== null) { - formData.append('source', urlType?.value); - } - formData.append('data', url); - if ( - redditData.client_id.length > 0 && - redditData.client_secret.length > 0 - ) { - formData.set('name', 'other'); - formData.set('data', JSON.stringify(redditData)); - } - if (urlType.value === 'github') { - formData.append('repo_url', repoUrl); // Pdeac - } - const apiHost = import.meta.env.VITE_API_HOST; + formData.append('source', ingestor.type); + + const defaultConfig = IngestorDefaultConfigs[ingestor.type].config; + + const mergedConfig = { ...defaultConfig, ...ingestor.config }; + const filteredConfig = Object.entries(mergedConfig).reduce( + (acc, [key, value]) => { + const field = IngestorFormSchemas[ingestor.type].find( + (f) => f.name === key, + ); + // Include the field if: + // 1. It's required, or + // 2. It's optional and has a non-empty value + if ( + field?.required || + (value !== undefined && value !== null && value !== '') + ) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); + + formData.append('data', JSON.stringify(filteredConfig)); + + const apiHost: string = import.meta.env.VITE_API_HOST; const xhr = new XMLHttpRequest(); - xhr.upload.addEventListener('progress', (event) => { - const progress = +((event.loaded / event.total) * 100).toFixed(2); - setProgress({ type: 'UPLOAD', percentage: progress }); + xhr.upload.addEventListener('progress', (event: ProgressEvent) => { + if (event.lengthComputable) { + const progressPercentage = +( + (event.loaded / event.total) * + 100 + ).toFixed(2); + setProgress({ type: 'UPLOAD', percentage: progressPercentage }); + } }); xhr.onload = () => { - const { task_id } = JSON.parse(xhr.responseText); - setTimeoutRef.current = setTimeout(() => { - setProgress({ type: 'TRAINING', percentage: 0, taskId: task_id }); + const response = JSON.parse(xhr.responseText) as { task_id: string }; + setTimeoutRef.current = window.setTimeout(() => { + setProgress({ + type: 'TRAINING', + percentage: 0, + taskId: response.task_id, + }); }, 3000); }; - xhr.open('POST', `${apiHost + '/api/remote'}`); + xhr.open('POST', `${apiHost}/api/remote`); xhr.send(formData); }; - const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, multiple: true, @@ -332,23 +494,68 @@ function Upload({ ], 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'], + 'image/png': ['.png'], + 'image/jpeg': ['.jpeg'], + 'image/jpg': ['.jpg'], }, }); - const handleChange = ( - e: React.ChangeEvent, + const isUploadDisabled = (): boolean => { + if (activeTab === 'file') { + return !docName?.trim() || files.length === 0; + } + if (activeTab === 'remote') { + if (!remoteName?.trim()) { + return true; + } + const formFields: FormField[] = IngestorFormSchemas[ingestor.type]; + for (const field of formFields) { + if (field.required) { + // Validate only required fields + const value = + ingestor.config[field.name as keyof typeof ingestor.config]; + + if (typeof value === 'string' && !value.trim()) { + return true; + } + + if ( + typeof value === 'number' && + (value === null || value === undefined || value <= 0) + ) { + return true; + } + + if (typeof value === 'boolean' && value === undefined) { + return true; + } + } + } + return false; + } + return true; + }; + const handleIngestorChange = ( + key: keyof IngestorConfig['config'], + value: string | number | boolean, ) => { - const { name, value } = e.target; - if (name === 'search_queries' && value.length > 0) { - setRedditData({ - ...redditData, - [name]: value.split(',').map((item) => item.trim()), - }); - } else - setRedditData({ - ...redditData, - [name]: name === 'number_posts' ? parseInt(value) : value, - }); + setIngestor((prevState) => ({ + ...prevState, + config: { + ...prevState.config, + [key]: value, + }, + })); + }; + const handleIngestorTypeChange = (type: IngestorType) => { + //Updates the ingestor seleced in dropdown and resets the config to the default config for that type + const defaultConfig = IngestorDefaultConfigs[type]; + + setIngestor({ + type, + name: defaultConfig.name, + config: defaultConfig.config, + }); }; let view; @@ -416,238 +623,116 @@ function Upload({

{t('modals.uploadDoc.info')}

-
+

{t('modals.uploadDoc.uploadedFiles')}

- {files.map((file) => ( -

- {file.name} -

- ))} - {files.length === 0 && ( -

- {t('none')} -

- )} +
+ {files.map((file) => ( +

+ {file.name} +

+ ))} + {files.length === 0 && ( +

+ {t('none')} +

+ )} +
)} {activeTab === 'remote' && ( <> - setUrlType(value) + selectedValue={ + urlOptions.find((opt) => opt.value === ingestor.type) || null + } + onSelect={(selected: { label: string; value: string }) => + handleIngestorTypeChange(selected.value as IngestorType) } size="w-full" rounded="3xl" /> - {urlType.label !== 'Reddit' && urlType.label !== 'GitHub' ? ( - <> - setUrlName(e.target.value)} - borderVariant="thin" - > -
- - {t('modals.uploadDoc.name')} - -
- setUrl(e.target.value)} - borderVariant="thin" - > -
- - {t('modals.uploadDoc.link')} - -
- - ) : urlType.label === 'GitHub' ? ( // P3f93 - <> - setUrlName(e.target.value)} - borderVariant="thin" - > -
- - {t('modals.uploadDoc.name')} - -
- setRepoUrl(e.target.value)} - borderVariant="thin" - > -
- - {t('modals.uploadDoc.repoUrl')} - -
- - ) : ( -
-
- -
- - {t('modals.uploadDoc.reddit.id')} - -
-
-
- -
- - {t('modals.uploadDoc.reddit.secret')} - -
-
-
- -
- - {t('modals.uploadDoc.reddit.agent')} - -
-
-
- -
- - {t('modals.uploadDoc.reddit.searchQueries')} - -
-
-
- -
- - {t('modals.uploadDoc.reddit.numberOfPosts')} - -
-
-
+ {/* Dynamically render form fields based on schema */} + + setRemoteName(e.target.value)} + borderVariant="thin" + placeholder="Name" + label="Name" + required={true} + /> + {renderFormFields()} + {IngestorFormSchemas[ingestor.type].some( + (field) => field.advanced, + ) && ( + )} )} - {activeTab && ( -
- {activeTab === 'file' ? ( - - ) : ( - - )} +
+ {activeTab && ( -
- )} + )} + {activeTab && ( + + )} +
); } return ( -
{ + close(); + setDocName(''); + setfiles([]); + setModalState('INACTIVE'); + setActiveTab(null); + }} > -
- {!isOnboarding && !progress && ( - - )} - {view} -
-
+ {view} + ); } diff --git a/frontend/src/upload/types/ingestor.ts b/frontend/src/upload/types/ingestor.ts new file mode 100644 index 00000000..cd709847 --- /dev/null +++ b/frontend/src/upload/types/ingestor.ts @@ -0,0 +1,146 @@ +export interface BaseIngestorConfig { + [key: string]: string | number | boolean | undefined; +} + +export interface RedditIngestorConfig extends BaseIngestorConfig { + client_id: string; + client_secret: string; + user_agent: string; + search_queries: string; + number_posts: number; +} + +export interface GithubIngestorConfig extends BaseIngestorConfig { + repo_url: string; +} + +export interface CrawlerIngestorConfig extends BaseIngestorConfig { + url: string; +} + +export interface UrlIngestorConfig extends BaseIngestorConfig { + url: string; +} + +export type IngestorType = 'crawler' | 'github' | 'reddit' | 'url'; + +export interface IngestorConfig { + type: IngestorType; + name: string; + config: + | RedditIngestorConfig + | GithubIngestorConfig + | CrawlerIngestorConfig + | UrlIngestorConfig; +} + +export type IngestorFormData = { + name: string; + user: string; + source: IngestorType; + data: string; +}; + +export type FieldType = 'string' | 'number' | 'enum' | 'boolean'; + +export interface FormField { + name: string; + label: string; + type: FieldType; + required?: boolean; + advanced?: boolean; + options?: { label: string; value: string }[]; +} + +export const IngestorFormSchemas: Record = { + crawler: [ + { + name: 'url', + label: 'URL', + type: 'string', + required: true, + }, + ], + url: [ + { + name: 'url', + label: 'URL', + type: 'string', + required: true, + }, + ], + reddit: [ + { + name: 'client_id', + label: 'Client ID', + type: 'string', + required: true, + }, + { + name: 'client_secret', + label: 'Client Secret', + type: 'string', + required: true, + }, + { + name: 'user_agent', + label: 'User Agent', + type: 'string', + required: true, + }, + { + name: 'search_queries', + label: 'Search Queries', + type: 'string', + required: true, + }, + { + name: 'number_posts', + label: 'Number of Posts', + type: 'number', + required: true, + }, + ], + github: [ + { + name: 'repo_url', + label: 'Repository URL', + type: 'string', + required: true, + }, + ], +}; + +export const IngestorDefaultConfigs: Record< + IngestorType, + Omit +> = { + crawler: { + name: '', + config: { + url: '', + } as CrawlerIngestorConfig, + }, + url: { + name: '', + config: { + url: '', + } as UrlIngestorConfig, + }, + reddit: { + name: '', + config: { + client_id: '', + client_secret: '', + user_agent: '', + search_queries: '', + number_posts: 10, + } as RedditIngestorConfig, + }, + github: { + name: '', + config: { + repo_url: '', + } as GithubIngestorConfig, + }, +}; diff --git a/frontend/src/utils/stringUtils.ts b/frontend/src/utils/stringUtils.ts new file mode 100644 index 00000000..e87a7af3 --- /dev/null +++ b/frontend/src/utils/stringUtils.ts @@ -0,0 +1,4 @@ +export function truncate(str: string, n: number) { + // slices long strings and ends with ... + return str.length > n ? str.slice(0, n - 1) + '...' : str; +} diff --git a/lexeu-competition.md b/lexeu-competition.md deleted file mode 100644 index e8824438..00000000 --- a/lexeu-competition.md +++ /dev/null @@ -1,55 +0,0 @@ -# LLM Document Analysis by [LexEU](https://www.lexeu.ai/) Competition - -## 🏆 Competition Details: - -Welcome to the LLM Document Analysis by [LexEU](https://www.lexeu.ai/) competition, part of Hacktoberfest! This challenge is designed for participants who can devise the best new retrieval or workflow method to analyze a document using EU laws. - -### 🏅 Prizes: -- **1st Place:** $200 + Special Holopin -- **2nd Place:** $100 + Special Holopin -- **3rd Place:** $50 + Special Holopin -- **Top 3 Winners:** Special Holopin - -### 📆 Timeline: -- **Competition Announcement:** 1st October -- **Deadline for Submissions:** 8th November -- **Results Announcement:** Early November - -## 📜 How to Participate: - -Participants are required to analyze a given test contract by scraping EU law data, storing it in a database, and retrieving only the relevant portions for analysis. The solution must be optimized for efficiency, using a maximum of 500k tokens. - -### Steps to Participate: - -1. **Download Test Contract:** You can download it via this [link](https://docs.google.com/document/d/198d7gFJbVWttkIS9ZRUs_PTKIjhsOUeR/edit?usp=sharing&ouid=107667025862106683614&rtpof=true&sd=true). -2. **Ingest EU Law Data:** Gather and store data in any format, it's available [here](https://eur-lex.europa.eu/browse/directories/legislation.html?displayProfile=lastConsDocProfile&classification=in-force). -3. **Optimized Data Retrieval:** Implement methods to retrieve only small, relevant portions of the law data for efficient analysis of the test contract. Try to create a custom retriever and a parser. -4. **Analyze the Contract:** Use your optimized retrieval method to analyze the test contract against the EU law data. -5. **Submission Criteria:** Your solution will be judged based on: - - Amount of corrections/inconsistencies found - - Number of tokens used (Maximum 500k tokens) - - Your submission should be a fork of DocsGPT where all the ingestion and analysis steps can be replicated - -### Submission Instructions: - -1. **Submit Your Work:** Once you finish your analysis, submit your solution by filling out this [form](https://airtable.com/appikMaJwdHhC1SDP/pagLWdew2HKpEaBKr/form). -2. **Private Test Contract:** Your solution will also be benchmarked against a private test contract to validate its efficiency and effectiveness. -3. **Evaluation:** The winners will be evaluated based on the effectiveness of their solution in identifying corrections/inconsistencies and the number of tokens used in the process. - -### Resources: - -- **Documentation:** Refer to our [Documentation](https://docs.docsgpt.cloud/) for guidance. -- **Discord Support:** Join our [Discord](https://discord.gg/n5BX8dh8rU) server for support and discussions related to the competition. -- Try looking at existing [retrievers](https://github.com/arc53/DocsGPT/tree/main/application/retriever) and maybe creating a custom one -- Try looking at [worker.py](https://github.com/arc53/DocsGPT/blob/main/application/worker.py) which ingests data and creating a custom one for ingesting EU law - -## 👥 Community and Support: - -If you need assistance, feel free to join our [Discord](https://discord.gg/n5BX8dh8rU) server. We're here to help newcomers, so don't hesitate to jump in and ask questions! - -## 📢 Announcement: -Stay tuned for updates, and good luck to all participants! - -Thank you for participating in the LLM Document Analysis by LexEU competition. Your innovative solutions could not only win you prizes but also contribute significantly to the DocsGPT community. Happy coding! 🚀 - ---- diff --git a/mock-backend/.gitignore b/mock-backend/.gitignore deleted file mode 100644 index bca646a7..00000000 --- a/mock-backend/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ - -# Elastic Beanstalk Files -.elasticbeanstalk/* -!.elasticbeanstalk/*.cfg.yml -!.elasticbeanstalk/*.global.yml diff --git a/mock-backend/Dockerfile b/mock-backend/Dockerfile deleted file mode 100644 index 588636a9..00000000 --- a/mock-backend/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM node:20.6.1-bullseye-slim - - -WORKDIR /app -COPY package*.json ./ -RUN npm install -COPY . . - -EXPOSE 8080 - -CMD [ "npm", "run", "start"] diff --git a/mock-backend/package-lock.json b/mock-backend/package-lock.json deleted file mode 100644 index 0671e4de..00000000 --- a/mock-backend/package-lock.json +++ /dev/null @@ -1,1379 +0,0 @@ -{ - "name": "mock-backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mock-backend", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "cors": "^2.8.5", - "json-server": "^0.17.4", - "uuid": "^9.0.1" - }, - "devDependencies": { - "@types/json-server": "^0.14.5" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.3", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", - "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.36", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", - "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.18", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", - "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.37", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz", - "integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", - "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==", - "dev": true - }, - "node_modules/@types/json-server": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/@types/json-server/-/json-server-0.14.5.tgz", - "integrity": "sha512-Eck8yX5a0PPPV5MhYg/1Xbklz0/BJ2ir874CReGiKsj22ZWD+XYP3ZXK6cTZ9Mqi099GmtIml/1X5aQJTcZr/Q==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/express": "*", - "@types/lowdb": "*" - } - }, - "node_modules/@types/lodash": { - "version": "4.14.199", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", - "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==", - "dev": true - }, - "node_modules/@types/lowdb": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@types/lowdb/-/lowdb-1.0.12.tgz", - "integrity": "sha512-m/hOfY7nuwo9V3yApvR6aJ3uZP6iNC74S7Bx5BWz0L7IrzjKyzUur/jEdlYWBWWVjmkCz+ECK9nk8UJoQa8aZw==", - "dev": true, - "dependencies": { - "@types/lodash": "*" - } - }, - "node_modules/@types/mime": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", - "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.8.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.3.tgz", - "integrity": "sha512-jxiZQFpb+NlH5kjW49vXxvxTjeeqlbsnTAdBTKpzEdPs9itay7MscYXz3Fo9VYFEsfQ6LJFitHad3faerLAjCw==", - "dev": true - }, - "node_modules/@types/qs": { - "version": "6.9.8", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", - "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz", - "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==", - "dev": true - }, - "node_modules/@types/send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz", - "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz", - "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==", - "dev": true, - "dependencies": { - "@types/http-errors": "*", - "@types/mime": "*", - "@types/node": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect-pause": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/connect-pause/-/connect-pause-0.1.1.tgz", - "integrity": "sha512-a1gSWQBQD73krFXdUEYJom2RTFrWUL3YvXDCRkyv//GVXc79cdW9MngtRuN9ih4FDKBtfJAJId+BbDuX+1rh2w==", - "engines": { - "node": "*" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/errorhandler": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz", - "integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==", - "dependencies": { - "accepts": "~1.3.7", - "escape-html": "~1.0.3" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express-urlrewrite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/express-urlrewrite/-/express-urlrewrite-1.4.0.tgz", - "integrity": "sha512-PI5h8JuzoweS26vFizwQl6UTF25CAHSggNv0J25Dn/IKZscJHWZzPrI5z2Y2jgOzIaw2qh8l6+/jUcig23Z2SA==", - "dependencies": { - "debug": "*", - "path-to-regexp": "^1.0.3" - } - }, - "node_modules/express-urlrewrite/node_modules/path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", - "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" - }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, - "node_modules/jju": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==" - }, - "node_modules/json-parse-helpfulerror": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", - "integrity": "sha512-XgP0FGR77+QhUxjXkwOMkC94k3WtqEBfcnjWqhRd82qTat4SWKRE+9kUnynz/shm3I4ea2+qISvTIeGTNU7kJg==", - "dependencies": { - "jju": "^1.1.0" - } - }, - "node_modules/json-server": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/json-server/-/json-server-0.17.4.tgz", - "integrity": "sha512-bGBb0WtFuAKbgI7JV3A864irWnMZSvBYRJbohaOuatHwKSRFUfqtQlrYMrB6WbalXy/cJabyjlb7JkHli6dYjQ==", - "dependencies": { - "body-parser": "^1.19.0", - "chalk": "^4.1.2", - "compression": "^1.7.4", - "connect-pause": "^0.1.1", - "cors": "^2.8.5", - "errorhandler": "^1.5.1", - "express": "^4.17.1", - "express-urlrewrite": "^1.4.0", - "json-parse-helpfulerror": "^1.0.3", - "lodash": "^4.17.21", - "lodash-id": "^0.14.1", - "lowdb": "^1.0.0", - "method-override": "^3.0.0", - "morgan": "^1.10.0", - "nanoid": "^3.1.23", - "please-upgrade-node": "^3.2.0", - "pluralize": "^8.0.0", - "server-destroy": "^1.0.1", - "yargs": "^17.0.1" - }, - "bin": { - "json-server": "lib/cli/bin.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash-id": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/lodash-id/-/lodash-id-0.14.1.tgz", - "integrity": "sha512-ikQPBTiq/d5m6dfKQlFdIXFzvThPi2Be9/AHxktOnDSfSxE1j9ICbBT5Elk1ke7HSTgM38LHTpmJovo9/klnLg==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/lowdb": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", - "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", - "dependencies": { - "graceful-fs": "^4.1.3", - "is-promise": "^2.1.0", - "lodash": "4", - "pify": "^3.0.0", - "steno": "^0.4.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/method-override": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", - "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", - "dependencies": { - "debug": "3.1.0", - "methods": "~1.1.2", - "parseurl": "~1.3.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/method-override/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" - }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "engines": { - "node": ">=4" - } - }, - "node_modules/please-upgrade-node": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", - "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", - "dependencies": { - "semver-compare": "^1.0.0" - } - }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==" - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/server-destroy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", - "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/steno": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", - "integrity": "sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w==", - "dependencies": { - "graceful-fs": "^4.1.3" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } - } - } -} diff --git a/mock-backend/package.json b/mock-backend/package.json deleted file mode 100644 index 9540fa0a..00000000 --- a/mock-backend/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "mock-backend", - "version": "1.0.0", - "description": "", - "main": "index.js", - "type": "module", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node src/server.js" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "cors": "^2.8.5", - "json-server": "^0.17.4", - "uuid": "^9.0.1" - }, - "devDependencies": { - "@types/json-server": "^0.14.5" - } -} diff --git a/mock-backend/src/mocks/db.json b/mock-backend/src/mocks/db.json deleted file mode 100644 index 36947158..00000000 --- a/mock-backend/src/mocks/db.json +++ /dev/null @@ -1,244 +0,0 @@ -{ - "combine": [ - { - "date": "default", - "description": "default", - "docLink": "default", - "fullName": "default", - "language": "default", - "location": "local", - "model": "openai_text-embedding-ada-002", - "name": "default", - "version": "" - }, - { - "date": "13/02/2023", - "description": "Serverless Framework, the serverless application framework for building web, mobile and IoT applications on AWS Lambda, Azure Functions, Google CloudFunctions & more!", - "docLink": "https://serverless.com/framework/docs/", - "fullName": "Serverless Framework", - "language": "serverless", - "location": "remote", - "name": "serverless framework", - "version": "3.27.0" - }, - { - "date": "15/02/2023", - "description": "Machine Learning in Python", - "docLink": "https://scikit-learn.org/stable/", - "fullName": "scikit-learn", - "language": "python", - "location": "remote", - "model": "openai_text-embedding-ada-002", - "name": "scikit-learn", - "version": "1.2.1" - }, - { - "date": "07/02/2023", - "description": "Machine Learning in Python", - "docLink": "https://scikit-learn.org/stable/", - "fullName": "scikit-learn", - "language": "python", - "location": "remote", - "name": "scikit-learn", - "version": "1.2.1" - }, - { - "date": "07/02/2023", - "description": "Pandas is alibrary providing high-performance, easy-to-use data structures and data analysis tools for the Python programming language.", - "docLink": "https://pandas.pydata.org/docs/", - "fullName": "Pandas", - "language": "python", - "location": "remote", - "model": "openai_text-embedding-ada-002", - "name": "pandas", - "version": "1.5.3" - }, - { - "date": "07/02/2023", - "description": "Pandas is alibrary providing high-performance, easy-to-use data structures and data analysis tools for the Python programming language.", - "docLink": "https://pandas.pydata.org/docs/", - "fullName": "Pandas", - "language": "python", - "location": "remote", - "name": "pandas", - "version": "1.5.3" - }, - { - "date": "29/02/2023", - "description": "Python is a programming language that lets you work quickly and integrate systems more effectively.", - "docLink": "https://docs.python.org/3/", - "fullName": "Python", - "language": "python", - "location": "remote", - "model": "huggingface_sentence-transformers-all-mpnet-base-v2", - "name": "python", - "version": "3.11.1" - }, - { - "date": "15/02/2023", - "description": "Python is a programming language that lets you work quickly and integrate systems more effectively.", - "docLink": "https://docs.python.org/3/", - "fullName": "Python", - "language": "python", - "location": "remote", - "model": "openai_text-embedding-ada-002", - "name": "python", - "version": "3.11.1" - }, - { - "date": "07/02/2023", - "description": "Python is a programming language that lets you work quickly and integrate systems more effectively.", - "docLink": "https://docs.python.org/3/", - "fullName": "Python", - "language": "python", - "location": "remote", - "name": "python", - "version": "3.11.1" - }, - { - "date": "08/02/2023", - "description": "GPT Index is a project consisting of a set of data structures designed to make it easier to use large external knowledge bases with LLMs.", - "docLink": "https://gpt-index.readthedocs.io/en/latest/index.html", - "fullName": "LangChain", - "language": "python", - "location": "remote", - "name": "gpt-index", - "version": "0.4.0" - }, - { - "date": "15/02/2023", - "description": "Large language models (LLMs) are emerging as a transformative technology, enabling developers to build applications that they previously could not.", - "docLink": "https://langchain.readthedocs.io/en/latest/index.html", - "fullName": "LangChain", - "language": "python", - "location": "remote", - "model": "openai_text-embedding-ada-002", - "name": "langchain", - "version": "0.0.87" - }, - { - "date": "07/02/2023", - "description": "Large language models (LLMs) are emerging as a transformative technology, enabling developers to build applications that they previously could not.", - "docLink": "https://langchain.readthedocs.io/en/latest/index.html", - "fullName": "LangChain", - "language": "python", - "location": "remote", - "name": "langchain", - "version": "0.0.79" - }, - { - "date": "13/03/2023", - "description": "Large language models (LLMs) are emerging as a transformative technology, enabling developers to build applications that they previously could not.", - "docLink": "https://langchain.readthedocs.io/en/latest/index.html", - "fullName": "LangChain", - "language": "python", - "location": "remote", - "model": "openai_text-embedding-ada-002", - "name": "langchain", - "version": "0.0.109" - }, - { - "date": "16/03/2023", - "description": "A JavaScript library for building user interfaces\nGet Started\n", - "docLink": "https://reactjs.org/", - "fullName": "React", - "language": "javascript", - "location": "remote", - "model": "openai_text-embedding-ada-002", - "name": "react", - "version": "v18.2.0" - }, - { - "date": "15/02/2023", - "description": "is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions.", - "docLink": "https://developer.mozilla.org/en-US/docs/Web/JavaScript", - "fullName": "JavaScript", - "language": "javascript", - "location": "remote", - "model": "openai_text-embedding-ada-002", - "name": "javascript", - "version": "ES2015" - }, - { - "date": "16/03/2023", - "description": "An approachable, performant and versatile framework for building web user interfaces. ", - "docLink": "https://vuejs.org/", - "fullName": "Vue.js", - "language": "javascript", - "location": "remote", - "model": "openai_text-embedding-ada-002", - "name": "vuejs", - "version": "v3.3.0" - }, - { - "date": "16/03/2023", - "description": "Get ready for a development environment that can finally catch up with you.", - "docLink": "https://vitejs.dev/", - "fullName": "Vite", - "language": "javascript", - "location": "remote", - "model": "openai_text-embedding-ada-002", - "name": "vitejs", - "version": "v4.2.0" - }, - { - "date": "15/02/2023", - "description": "Solidity is an object-oriented, high-level language for implementing smart contracts.", - "docLink": "https://docs.soliditylang.org/en/v0.8.18/", - "fullName": "Solidity", - "language": "ethereum", - "location": "remote", - "model": "openai_text-embedding-ada-002", - "name": "solidity", - "version": "0.8.18" - }, - { - "date": "07/02/2023", - "description": "Solidity is an object-oriented, high-level language for implementing smart contracts.", - "docLink": "https://docs.soliditylang.org/en/v0.8.18/", - "fullName": "Solidity", - "language": "ethereum", - "location": "remote", - "name": "solidity", - "version": "0.8.18" - }, - { - "date": "28/02/2023", - "description": "GPT-powered chat for documentation search & assistance. ", - "docLink": "https://github.com/arc53/DocsGPT/wiki", - "fullName": "DocsGPT", - "language": "docsgpt", - "location": "remote", - "model": "huggingface_sentence-transformers-all-mpnet-base-v2", - "name": "docsgpt", - "version": "0.1.0" - }, - { - "date": "28/02/2023", - "description": "GPT-powered chat for documentation search & assistance. ", - "docLink": "https://github.com/arc53/DocsGPT/wiki", - "fullName": "DocsGPT", - "language": "docsgpt", - "location": "remote", - "model": "openai_text-embedding-ada-002", - "name": "docsgpt", - "version": "0.1.0" - } - ], - "conversations": [ - { - "id": "65cf39c936523eea21ebe117", - "name": "Request clarification" - }, - { - "id": "65cf39ba36523eea21ebe116", - "name": "Clarification request" - }, - { - "id": "65cf37e97d527c332bbac933", - "name": "Greetings, assistance inquiry." - }], - "docs_check": { - "status": "loaded" - } -} diff --git a/mock-backend/src/mocks/routes.json b/mock-backend/src/mocks/routes.json deleted file mode 100644 index 5bcccf0f..00000000 --- a/mock-backend/src/mocks/routes.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "/api/*": "/$1", - "/get_conversations": "/conversations", - "/get_single_conversation?id=:id": "/conversations/:id", - "/delete_conversation?id=:id": "/conversations/:id", - "/conversations?id=:id": "/conversations/:id" -} \ No newline at end of file diff --git a/mock-backend/src/server.js b/mock-backend/src/server.js deleted file mode 100644 index 93c326b1..00000000 --- a/mock-backend/src/server.js +++ /dev/null @@ -1,131 +0,0 @@ -import jsonServer from "json-server"; -import routes from "./mocks/routes.json" assert { type: "json" }; -import { v4 as uuid } from "uuid"; -import cors from 'cors' -const server = jsonServer.create(); -const router = jsonServer.router("./src/mocks/db.json"); -const middlewares = jsonServer.defaults(); - -const localStorage = []; - -server.use(middlewares); -server.use(cors({ origin: ['*'] })) -server.use(jsonServer.rewriter(routes)); - -server.use((req, res, next) => { - if (req.method === "POST") { - if (req.url.includes("/delete_conversation")) { - req.method = "DELETE"; - } else if (req.url === "/upload") { - const taskId = uuid(); - localStorage.push(taskId); - } - } - next(); -}); - -router.render = (req, res) => { - if (req.url === "/feedback") { - res.status(200).jsonp({ status: "ok" }); - } else if (req.url === "/upload") { - res.status(200).jsonp({ - status: "ok", - task_id: localStorage[localStorage.length - 1], - }); - } else if (req.url.includes("/task_status")) { - const taskId = req.query["task_id"]; - const taskIdExists = localStorage.includes(taskId); - if (taskIdExists) { - res.status(200).jsonp({ - result: { - directory: "temp", - filename: "install.rst", - formats: [".rst", ".md", ".pdf"], - name_job: "somename", - user: "local", - }, - status: "SUCCESS", - }); - } else { - res.status(404).jsonp({}); - } - } else if (req.url === "/stream" && req.method === "POST") { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive' - }); - const message = ('Hi, How are you today?').split(' '); - let index = 0; - const interval = setInterval(() => { - if (index < message.length) { - res.write(`data: {"answer": "${message[index++]} "}\n`); - } else { - res.write(`data: {"type": "id", "id": "65cbc39d11f077b9eeb06d26"}\n`) - res.write(`data: {"type": "end"}\n`) - clearInterval(interval); // Stop the interval once the message is fully streamed - res.end(); // End the response - } - }, 500); // Send a word every 1 second - } - else if (req.url === '/search' && req.method === 'POST') { - res.status(200).json( - [ - { - "text": "\n\n/api/answer\nIt's a POST request that sends a JSON in body with 4 values. It will receive an answer for a user provided question.\n", - "title": "API-docs.md" - }, - { - "text": "\n\nOur Standards\n\nExamples of behavior that contribute to a positive environment for our\ncommunity include:\n* Demonstrating empathy and kindness towards other people\n", - "title": "How-to-use-different-LLM.md" - } - ] - ) - } - else if (req.url === '/get_prompts' && req.method === 'GET') { - res.status(200).json([ - { - "id": "default", - "name": "default", - "type": "public" - }, - { - "id": "creative", - "name": "creative", - "type": "public" - }, - { - "id": "strict", - "name": "strict", - "type": "public" - } - ]); - } - else if (req.url.startsWith('/get_single_prompt') && req.method==='GET') { - const id = req.query.id; - console.log('hre'); - if (id === 'creative') - res.status(200).json({ - "content": "You are a DocsGPT, friendly and helpful AI assistant by Arc53 that provides help with documents. You give thorough answers with code examples if possible." - }) - else if (id === 'strict') { - res.status(200).json({ - "content": "You are an AI Assistant, DocsGPT, adept at offering document assistance. \nYour expertise lies in providing answer on top of provided context." - }) - } - else { - res.status(200).json({ - "content": "You are a helpful AI assistant, DocsGPT, specializing in document assistance, designed to offer detailed and informative responses." - }) - } - } - else { - res.status(res.statusCode).jsonp(res.locals.data); - } -}; - -server.use(router); - -server.listen(8080, () => { - console.log("JSON Server is running"); -}); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1d8f28b5..00000000 --- a/package-lock.json +++ /dev/null @@ -1,1716 +0,0 @@ -{ - "name": "DocsGPT", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "devDependencies": { - "eslint": "^8.53.0", - "lint-staged": "^15.1.0", - "prettier": "^3.1.0" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", - "dev": true - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", - "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", - "dev": true, - "dependencies": { - "type-fest": "^1.0.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "dev": true, - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "dev": true, - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, - "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", - "@humanwhocodes/config-array": "^0.11.13", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lint-staged": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.1.0.tgz", - "integrity": "sha512-ZPKXWHVlL7uwVpy8OZ7YQjYDAuO5X4kMh0XgZvPNxLcCCngd0PO5jKQyy3+s4TL2EnHoIXIzP1422f/l3nZKMw==", - "dev": true, - "dependencies": { - "chalk": "5.3.0", - "commander": "11.1.0", - "debug": "4.3.4", - "execa": "8.0.1", - "lilconfig": "2.1.0", - "listr2": "7.0.2", - "micromatch": "4.0.5", - "pidtree": "0.6.0", - "string-argv": "0.3.2", - "yaml": "2.3.4" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/listr2": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-7.0.2.tgz", - "integrity": "sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==", - "dev": true, - "dependencies": { - "cli-truncate": "^3.1.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^5.0.1", - "rfdc": "^1.3.0", - "wrap-ansi": "^8.1.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/log-update": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", - "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", - "dev": true, - "dependencies": { - "ansi-escapes": "^5.0.0", - "cli-cursor": "^4.0.0", - "slice-ansi": "^5.0.0", - "strip-ansi": "^7.0.1", - "wrap-ansi": "^8.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 78820bd8..00000000 --- a/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "devDependencies": { - "eslint": "^8.53.0", - "lint-staged": "^15.1.0", - "prettier": "^3.1.0" - } -} diff --git a/run-with-docker-compose.sh b/run-with-docker-compose.sh deleted file mode 100755 index 145b1e23..00000000 --- a/run-with-docker-compose.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -source .env - -if [[ -n "$OPENAI_API_BASE" ]] && [[ -n "$OPENAI_API_VERSION" ]] && [[ -n "$AZURE_DEPLOYMENT_NAME" ]] && [[ -n "$AZURE_EMBEDDINGS_DEPLOYMENT_NAME" ]]; then - echo "Running Azure Configuration" - docker compose -f docker-compose-azure.yaml up --build -else - echo "Running Plain Configuration" - docker compose up --build -fi diff --git a/scripts/__init__.py b/scripts/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/code_docs_gen.py b/scripts/code_docs_gen.py deleted file mode 100644 index c5c2d141..00000000 --- a/scripts/code_docs_gen.py +++ /dev/null @@ -1,95 +0,0 @@ -import ast -import json -from pathlib import Path - -import dotenv -from langchain_community.llms import OpenAI -from langchain.prompts import PromptTemplate - -dotenv.load_dotenv() - -ps = list(Path("inputs").glob("**/*.py")) -data = [] -sources = [] -for p in ps: - with open(p) as f: - data.append(f.read()) - sources.append(p) - - -def get_functions_in_class(node): - functions = [] - functions_code = [] - for child in node.body: - if isinstance(child, ast.FunctionDef): - functions.append(child.name) - functions_code.append(ast.unparse(child)) - - return functions, functions_code - - -def get_classes_and_functions(source_code): - tree = ast.parse(source_code) - classes = {} - for node in tree.body: - if isinstance(node, ast.ClassDef): - class_name = node.name - function_name, function = get_functions_in_class(node) - # join function name and function code - functions = dict(zip(function_name, function)) - classes[class_name] = functions - return classes - - -structure_dict = {} -c1 = 0 -for code in data: - classes = get_classes_and_functions(ast.parse(code)) - source = str(sources[c1]) - structure_dict[source] = classes - c1 += 1 - -# save the structure dict as json -with open('structure_dict.json', 'w') as f: - json.dump(structure_dict, f) - -if not Path("outputs").exists(): - Path("outputs").mkdir() - -c1 = len(structure_dict) -c2 = 0 -for source, classes in structure_dict.items(): - c2 += 1 - print(f"Processing file {c2}/{c1}") - f1 = len(classes) - f2 = 0 - for class_name, functions in classes.items(): - f2 += 1 - print(f"Processing class {f2}/{f1}") - source_w = source.replace("inputs/", "") - source_w = source_w.replace(".py", ".txt") - if not Path(f"outputs/{source_w}").exists(): - with open(f"outputs/{source_w}", "w") as f: - f.write(f"Class: {class_name}") - else: - with open(f"outputs/{source_w}", "a") as f: - f.write(f"\n\nClass: {class_name}") - # append class name to the front - for function in functions: - b1 = len(functions) - b2 = 0 - print(f"Processing function {b2}/{b1}") - b2 += 1 - prompt = PromptTemplate( - input_variables=["code"], - template="Code: \n{code}, \nDocumentation: ", - ) - llm = OpenAI(temperature=0) - response = llm(prompt.format(code=functions[function])) - - if not Path(f"outputs/{source_w}").exists(): - with open(f"outputs/{source_w}", "w") as f: - f.write(f"Function: {functions[function]}, \nDocumentation: {response}") - else: - with open(f"outputs/{source_w}", "a") as f: - f.write(f"\n\nFunction: {functions[function]}, \nDocumentation: {response}") diff --git a/scripts/ingest.py b/scripts/ingest.py deleted file mode 100644 index 8c74fd03..00000000 --- a/scripts/ingest.py +++ /dev/null @@ -1,128 +0,0 @@ -import os -import sys -from collections import defaultdict -from typing import List, Optional - -import dotenv -import nltk -import typer - -from parser.file.bulk import SimpleDirectoryReader -from parser.java2doc import extract_functions_and_classes as extract_java -from parser.js2doc import extract_functions_and_classes as extract_js -from parser.open_ai_func import call_openai_api, get_user_permission -from parser.py2doc import extract_functions_and_classes as extract_py -from parser.py2doc import transform_to_docs -from parser.schema.base import Document -from parser.token_func import group_split - -dotenv.load_dotenv() - -app = typer.Typer(add_completion=False) - -nltk.download('punkt', quiet=True) -nltk.download('averaged_perceptron_tagger', quiet=True) - - -def metadata_from_filename(title): - return {'title': title} - -# Splits all files in specified folder to documents -@app.command() -def ingest(yes: bool = typer.Option(False, "-y", "--yes", prompt=False, - help="Whether to skip price confirmation"), - dir: Optional[List[str]] = typer.Option(["inputs"], - help="""List of paths to directory for index creation. - E.g. --dir inputs --dir inputs2"""), - file: Optional[List[str]] = typer.Option(None, - help="""File paths to use (Optional; overrides dir). - E.g. --file inputs/1.md --file inputs/2.md"""), - recursive: Optional[bool] = typer.Option(True, help="Whether to recursively search in subdirectories."), - limit: Optional[int] = typer.Option(None, help="Maximum number of files to read."), - formats: Optional[List[str]] = typer.Option([".rst", ".md"], - help="""List of required extensions (list with .) - Currently supported: - .rst, .md, .pdf, .docx, .csv, .epub, .html, .mdx"""), - exclude: Optional[bool] = typer.Option(True, help="Whether to exclude hidden files (dotfiles)."), - sample: Optional[bool] = typer.Option(False, - help="Whether to output sample of the first 5 split documents."), - token_check: Optional[bool] = typer.Option(True, help="Whether to group small documents and split large."), - min_tokens: Optional[int] = typer.Option(150, help="Minimum number of tokens to not group."), - max_tokens: Optional[int] = typer.Option(2000, help="Maximum number of tokens to not split."), - ): - """ - Creates index from specified location or files. - By default /inputs folder is used, .rst and .md are parsed. - """ - - def process_one_docs(directory, folder_name): - raw_docs = SimpleDirectoryReader(input_dir=directory, input_files=file, recursive=recursive, - required_exts=formats, num_files_limit=limit, - exclude_hidden=exclude, file_metadata=metadata_from_filename).load_data() - - # Here we split the documents, as needed, into smaller chunks. - # We do this due to the context limits of the LLMs. - raw_docs = group_split(documents=raw_docs, min_tokens=min_tokens, max_tokens=max_tokens, - token_check=token_check) - # Old method - # text_splitter = RecursiveCharacterTextSplitter() - # docs = text_splitter.split_documents(raw_docs) - - # Sample feature - if sample: - for i in range(min(5, len(raw_docs))): - print(raw_docs[i].text) - - docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs] - - # Here we check for command line arguments for bot calls. - # If no argument exists or the yes is not True, then the - # user permission is requested to call the API. - if len(sys.argv) > 1 and yes: - call_openai_api(docs, folder_name) - else: - get_user_permission(docs, folder_name) - - - folder_counts = defaultdict(int) - folder_names = [] - for dir_path in dir: - folder_name = os.path.basename(os.path.normpath(dir_path)) - folder_counts[folder_name] += 1 - if folder_counts[folder_name] > 1: - folder_name = f"{folder_name}_{folder_counts[folder_name]}" - folder_names.append(folder_name) - - for directory, folder_name in zip(dir, folder_names): - process_one_docs(directory, folder_name) - - -@app.command() -def convert(dir: Optional[str] = typer.Option("inputs", - help="""Path to directory to make documentation for. - E.g. --dir inputs """), - formats: Optional[str] = typer.Option("py", - help="""Required language. - py, js, java supported for now""")): - """ - Creates documentation linked to original functions from specified location. - By default /inputs folder is used, .py is parsed. - """ - # Using a dictionary to map between the formats and their respective extraction functions - # makes the code more scalable. When adding more formats in the future, - # you only need to update the extraction_functions dictionary. - extraction_functions = { - 'py': extract_py, - 'js': extract_js, - 'java': extract_java - } - - if formats in extraction_functions: - functions_dict, classes_dict = extraction_functions[formats](dir) - else: - raise Exception("Sorry, language not supported yet") - transform_to_docs(functions_dict, classes_dict, formats, dir) - - -if __name__ == "__main__": - app() diff --git a/scripts/old/__init__.py b/scripts/old/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/old/ingest_rst.py b/scripts/old/ingest_rst.py deleted file mode 100644 index 816ac6eb..00000000 --- a/scripts/old/ingest_rst.py +++ /dev/null @@ -1,90 +0,0 @@ -import pickle -import sys -from argparse import ArgumentParser -from pathlib import Path - -import dotenv -import faiss -import tiktoken -from langchain_openai import OpenAIEmbeddings -from langchain.text_splitter import CharacterTextSplitter -from langchain.vectorstores import FAISS - - -def num_tokens_from_string(string: str, encoding_name: str) -> int: - # Function to convert string to tokens and estimate user cost. - encoding = tiktoken.get_encoding(encoding_name) - num_tokens = len(encoding.encode(string)) - total_price = ((num_tokens / 1000) * 0.0004) - return num_tokens, total_price - - -def call_openai_api(): - # Function to create a vector store from the documents and save it to disk. - store = FAISS.from_texts(docs, OpenAIEmbeddings(), metadatas=metadatas) - faiss.write_index(store.index, "docs.index") - store.index = None - with open("faiss_store.pkl", "wb") as f: - pickle.dump(store, f) - - -def get_user_permission(): - # Function to ask user permission to call the OpenAI api and spend their OpenAI funds. - # Here we convert the docs list to a string and calculate the number of OpenAI tokens the string represents. - docs_content = (" ".join(docs)) - tokens, total_price = num_tokens_from_string(string=docs_content, encoding_name="cl100k_base") - # Here we print the number of tokens and the approx user cost with some visually appealing formatting. - print(f"Number of Tokens = {format(tokens, ',d')}") - print(f"Approx Cost = ${format(total_price, ',.2f')}") - # Here we check for user permission before calling the API. - user_input = input("Price Okay? (Y/N) \n").lower() - if user_input == "y": - call_openai_api() - elif user_input == "": - call_openai_api() - else: - print("The API was not called. No money was spent.") - - -# Load .env file -dotenv.load_dotenv() - -ap = ArgumentParser("Script for training DocsGPT on .rst documentation files.") -ap.add_argument("-i", "--inputs", - type=str, - default="inputs", - help="Directory containing documentation files") -args = ap.parse_args() - -# Here we load in the data in the format that Notion exports it in. -ps = list(Path(args.inputs).glob("**/*.rst")) - -# parse all child directories -data = [] -sources = [] -for p in ps: - with open(p) as f: - data.append(f.read()) - sources.append(p) - -# Here we split the documents, as needed, into smaller chunks. -# We do this due to the context limits of the LLMs. -text_splitter = CharacterTextSplitter(chunk_size=1500, separator="\n") -docs = [] -metadatas = [] -for i, d in enumerate(data): - splits = text_splitter.split_text(d) - docs.extend(splits) - metadatas.extend([{"source": sources[i]}] * len(splits)) - -# Here we check for command line arguments for bot calls. -# If no argument exists or the permission_bypass_flag argument is not '-y', -# user permission is requested to call the API. -if len(sys.argv) > 1: - permission_bypass_flag = sys.argv[1] - if permission_bypass_flag == '-y': - call_openai_api() - else: - get_user_permission() -else: - get_user_permission() diff --git a/scripts/old/ingest_rst_sphinx.py b/scripts/old/ingest_rst_sphinx.py deleted file mode 100644 index ddafda85..00000000 --- a/scripts/old/ingest_rst_sphinx.py +++ /dev/null @@ -1,133 +0,0 @@ -import os -import pickle -import shutil -import sys -from argparse import ArgumentParser -from pathlib import Path - -import dotenv -import faiss -import tiktoken -from langchain_openai import OpenAIEmbeddings -from langchain.text_splitter import CharacterTextSplitter -from langchain.vectorstores import FAISS -from sphinx.cmd.build import main as sphinx_main - - -def convert_rst_to_txt(src_dir, dst_dir): - # Check if the source directory exists - if not os.path.exists(src_dir): - raise Exception("Source directory does not exist") - # Walk through the source directory - for root, dirs, files in os.walk(src_dir): - for file in files: - # Check if the file has .rst extension - if file.endswith(".rst"): - # Construct the full path of the file - src_file = os.path.join(root, file.replace(".rst", "")) - # Convert the .rst file to .txt file using sphinx-build - args = f". -b text -D extensions=sphinx.ext.autodoc " \ - f"-D master_doc={src_file} " \ - f"-D source_suffix=.rst " \ - f"-C {dst_dir} " - sphinx_main(args.split()) - elif file.endswith(".md"): - # Rename the .md file to .rst file - src_file = os.path.join(root, file) - dst_file = os.path.join(root, file.replace(".md", ".rst")) - os.rename(src_file, dst_file) - # Convert the .rst file to .txt file using sphinx-build - args = f". -b text -D extensions=sphinx.ext.autodoc " \ - f"-D master_doc={dst_file} " \ - f"-D source_suffix=.rst " \ - f"-C {dst_dir} " - sphinx_main(args.split()) - - -def num_tokens_from_string(string: str, encoding_name: str) -> int: - # Function to convert string to tokens and estimate user cost. - encoding = tiktoken.get_encoding(encoding_name) - num_tokens = len(encoding.encode(string)) - total_price = ((num_tokens / 1000) * 0.0004) - return num_tokens, total_price - - -def call_openai_api(): - # Function to create a vector store from the documents and save it to disk. - store = FAISS.from_texts(docs, OpenAIEmbeddings(), metadatas=metadatas) - faiss.write_index(store.index, "docs.index") - store.index = None - with open("faiss_store.pkl", "wb") as f: - pickle.dump(store, f) - - -def get_user_permission(): - # Function to ask user permission to call the OpenAI api and spend their OpenAI funds. - # Here we convert the docs list to a string and calculate the number of OpenAI tokens the string represents. - docs_content = (" ".join(docs)) - tokens, total_price = num_tokens_from_string(string=docs_content, encoding_name="cl100k_base") - # Here we print the number of tokens and the approx user cost with some visually appealing formatting. - print(f"Number of Tokens = {format(tokens, ',d')}") - print(f"Approx Cost = ${format(total_price, ',.2f')}") - # Here we check for user permission before calling the API. - user_input = input("Price Okay? (Y/N) \n").lower() - if user_input == "y": - call_openai_api() - elif user_input == "": - call_openai_api() - else: - print("The API was not called. No money was spent.") - - -ap = ArgumentParser("Script for training DocsGPT on Sphinx documentation") -ap.add_argument("-i", "--inputs", - type=str, - default="inputs", - help="Directory containing documentation files") -args = ap.parse_args() - -# Load .env file -dotenv.load_dotenv() - -# Directory to vector -src_dir = args.inputs -dst_dir = "tmp" - -convert_rst_to_txt(src_dir, dst_dir) - -# Here we load in the data in the format that Notion exports it in. -ps = list(Path("tmp/" + src_dir).glob("**/*.txt")) - -# parse all child directories -data = [] -sources = [] -for p in ps: - with open(p) as f: - data.append(f.read()) - sources.append(p) - -# Here we split the documents, as needed, into smaller chunks. -# We do this due to the context limits of the LLMs. -text_splitter = CharacterTextSplitter(chunk_size=1500, separator="\n") -docs = [] -metadatas = [] -for i, d in enumerate(data): - splits = text_splitter.split_text(d) - docs.extend(splits) - metadatas.extend([{"source": sources[i]}] * len(splits)) - -# Here we check for command line arguments for bot calls. -# If no argument exists or the permission_bypass_flag argument is not '-y', -# user permission is requested to call the API. -if len(sys.argv) > 1: - permission_bypass_flag = sys.argv[1] - if permission_bypass_flag == '-y': - call_openai_api() - else: - get_user_permission() -else: - get_user_permission() - -# Delete tmp folder -# Commented out for now -shutil.rmtree(dst_dir) diff --git a/scripts/parser/__init__.py b/scripts/parser/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/scripts/parser/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/scripts/parser/file/__init__.py b/scripts/parser/file/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/parser/file/base.py b/scripts/parser/file/base.py deleted file mode 100644 index 2fe9a75d..00000000 --- a/scripts/parser/file/base.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Base reader class.""" -from abc import abstractmethod -from typing import Any, List - -from langchain.docstore.document import Document as LCDocument -from parser.schema.base import Document - - -class BaseReader: - """Utilities for loading data from a directory.""" - - @abstractmethod - def load_data(self, *args: Any, **load_kwargs: Any) -> List[Document]: - """Load data from the input directory.""" - - def load_langchain_documents(self, **load_kwargs: Any) -> List[LCDocument]: - """Load data in LangChain document format.""" - docs = self.load_data(**load_kwargs) - return [d.to_langchain_format() for d in docs] diff --git a/scripts/parser/file/base_parser.py b/scripts/parser/file/base_parser.py deleted file mode 100644 index 753a56f9..00000000 --- a/scripts/parser/file/base_parser.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Base parser and config class.""" - -from abc import abstractmethod -from pathlib import Path -from typing import Dict, List, Optional, Union - - -class BaseParser: - """Base class for all parsers.""" - - def __init__(self, parser_config: Optional[Dict] = None): - """Init params.""" - self._parser_config = parser_config - - def init_parser(self) -> None: - """Init parser and store it.""" - parser_config = self._init_parser() - self._parser_config = parser_config - - @property - def parser_config_set(self) -> bool: - """Check if parser config is set.""" - return self._parser_config is not None - - @property - def parser_config(self) -> Dict: - """Check if parser config is set.""" - if self._parser_config is None: - raise ValueError("Parser config not set.") - return self._parser_config - - @abstractmethod - def _init_parser(self) -> Dict: - """Initialize the parser with the config.""" - - @abstractmethod - def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, List[str]]: - """Parse file.""" diff --git a/scripts/parser/file/bulk.py b/scripts/parser/file/bulk.py deleted file mode 100644 index 8f59819e..00000000 --- a/scripts/parser/file/bulk.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Simple reader that reads files of different formats from a directory.""" -import logging -from parser.file.base import BaseReader -from parser.file.base_parser import BaseParser -from parser.file.docs_parser import DocxParser, PDFParser -from parser.file.epub_parser import EpubParser -from parser.file.html_parser import HTMLParser -from parser.file.markdown_parser import MarkdownParser -from parser.file.rst_parser import RstParser -from parser.file.tabular_parser import PandasCSVParser -from parser.schema.base import Document -from pathlib import Path -from typing import Callable, Dict, List, Optional, Union - -DEFAULT_FILE_EXTRACTOR: Dict[str, BaseParser] = { - ".pdf": PDFParser(), - ".docx": DocxParser(), - ".csv": PandasCSVParser(), - ".epub": EpubParser(), - ".md": MarkdownParser(), - ".rst": RstParser(), - ".html": HTMLParser(), - ".mdx": MarkdownParser(), -} - - -class SimpleDirectoryReader(BaseReader): - """Simple directory reader. - - Can read files into separate documents, or concatenates - files into one document text. - - Args: - input_dir (str): Path to the directory. - input_files (List): List of file paths to read (Optional; overrides input_dir) - exclude_hidden (bool): Whether to exclude hidden files (dotfiles). - errors (str): how encoding and decoding errors are to be handled, - see https://docs.python.org/3/library/functions.html#open - recursive (bool): Whether to recursively search in subdirectories. - False by default. - required_exts (Optional[List[str]]): List of required extensions. - Default is None. - file_extractor (Optional[Dict[str, BaseParser]]): A mapping of file - extension to a BaseParser class that specifies how to convert that file - to text. See DEFAULT_FILE_EXTRACTOR. - num_files_limit (Optional[int]): Maximum number of files to read. - Default is None. - file_metadata (Optional[Callable[str, Dict]]): A function that takes - in a filename and returns a Dict of metadata for the Document. - Default is None. - """ - - def __init__( - self, - input_dir: Optional[str] = None, - input_files: Optional[List] = None, - exclude_hidden: bool = True, - errors: str = "ignore", - recursive: bool = True, - required_exts: Optional[List[str]] = None, - file_extractor: Optional[Dict[str, BaseParser]] = None, - num_files_limit: Optional[int] = None, - file_metadata: Optional[Callable[[str], Dict]] = None, - ) -> None: - """Initialize with parameters.""" - super().__init__() - - if not input_dir and not input_files: - raise ValueError("Must provide either `input_dir` or `input_files`.") - - self.errors = errors - - self.recursive = recursive - self.exclude_hidden = exclude_hidden - self.required_exts = required_exts - self.num_files_limit = num_files_limit - print("input_files") - print(input_files) - - if input_files: - self.input_files = [] - for path in input_files: - input_file = Path(path) - self.input_files.append(input_file) - elif input_dir: - self.input_dir = Path(input_dir) - self.input_files = self._add_files(self.input_dir) - - self.file_extractor = file_extractor or DEFAULT_FILE_EXTRACTOR - self.file_metadata = file_metadata - - def _add_files(self, input_dir: Path) -> List[Path]: - """Add files.""" - input_files = sorted(input_dir.iterdir()) - new_input_files = [] - dirs_to_explore = [] - for input_file in input_files: - if input_file.is_dir(): - if self.recursive: - dirs_to_explore.append(input_file) - elif self.exclude_hidden and input_file.name.startswith("."): - continue - elif ( - self.required_exts is not None - and input_file.suffix not in self.required_exts - ): - continue - else: - new_input_files.append(input_file) - - for dir_to_explore in dirs_to_explore: - sub_input_files = self._add_files(dir_to_explore) - new_input_files.extend(sub_input_files) - - if self.num_files_limit is not None and self.num_files_limit > 0: - new_input_files = new_input_files[0: self.num_files_limit] - - # print total number of files added - logging.debug( - f"> [SimpleDirectoryReader] Total files added: {len(new_input_files)}" - ) - - return new_input_files - - def load_data(self, concatenate: bool = False) -> List[Document]: - """Load data from the input directory. - - Args: - concatenate (bool): whether to concatenate all files into one document. - If set to True, file metadata is ignored. - False by default. - - Returns: - List[Document]: A list of documents. - - """ - data: Union[str, List[str]] = "" - data_list: List[str] = [] - metadata_list = [] - for input_file in self.input_files: - if input_file.suffix in self.file_extractor: - parser = self.file_extractor[input_file.suffix] - if not parser.parser_config_set: - parser.init_parser() - data = parser.parse_file(input_file, errors=self.errors) - else: - # do standard read - with open(input_file, "r", errors=self.errors) as f: - data = f.read() - if isinstance(data, List): - data_list.extend(data) - if self.file_metadata is not None: - for _ in range(len(data)): - metadata_list.append(self.file_metadata(str(input_file))) - else: - data_list.append(str(data)) - if self.file_metadata is not None: - metadata_list.append(self.file_metadata(str(input_file))) - - - - if concatenate: - return [Document("\n".join(data_list))] - elif self.file_metadata is not None: - return [Document(d, extra_info=m) for d, m in zip(data_list, metadata_list)] - else: - return [Document(d) for d in data_list] diff --git a/scripts/parser/file/docs_parser.py b/scripts/parser/file/docs_parser.py deleted file mode 100644 index 0cde4076..00000000 --- a/scripts/parser/file/docs_parser.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Docs parser. - -Contains parsers for docx, pdf files. - -""" -from pathlib import Path -from typing import Dict - -from parser.file.base_parser import BaseParser - - -class PDFParser(BaseParser): - """PDF parser.""" - - def _init_parser(self) -> Dict: - """Init parser.""" - return {} - - def parse_file(self, file: Path, errors: str = "ignore") -> str: - """Parse file.""" - try: - import PyPDF2 - except ImportError: - raise ValueError("PyPDF2 is required to read PDF files.") - text_list = [] - with open(file, "rb") as fp: - # Create a PDF object - pdf = PyPDF2.PdfReader(fp) - - # Get the number of pages in the PDF document - num_pages = len(pdf.pages) - - # Iterate over every page - for page in range(num_pages): - # Extract the text from the page - page_text = pdf.pages[page].extract_text() - text_list.append(page_text) - text = "\n".join(text_list) - - return text - - -class DocxParser(BaseParser): - """Docx parser.""" - - def _init_parser(self) -> Dict: - """Init parser.""" - return {} - - def parse_file(self, file: Path, errors: str = "ignore") -> str: - """Parse file.""" - try: - import docx2txt - except ImportError: - raise ValueError("docx2txt is required to read Microsoft Word files.") - - text = docx2txt.process(file) - - return text diff --git a/scripts/parser/file/epub_parser.py b/scripts/parser/file/epub_parser.py deleted file mode 100644 index 6ece5ecf..00000000 --- a/scripts/parser/file/epub_parser.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Epub parser. - -Contains parsers for epub files. -""" - -from pathlib import Path -from typing import Dict - -from parser.file.base_parser import BaseParser - - -class EpubParser(BaseParser): - """Epub Parser.""" - - def _init_parser(self) -> Dict: - """Init parser.""" - return {} - - def parse_file(self, file: Path, errors: str = "ignore") -> str: - """Parse file.""" - try: - import ebooklib - from ebooklib import epub - except ImportError: - raise ValueError("`EbookLib` is required to read Epub files.") - try: - import html2text - except ImportError: - raise ValueError("`html2text` is required to parse Epub files.") - - text_list = [] - book = epub.read_epub(file, options={"ignore_ncx": True}) - - # Iterate through all chapters. - for item in book.get_items(): - # Chapters are typically located in epub documents items. - if item.get_type() == ebooklib.ITEM_DOCUMENT: - text_list.append( - html2text.html2text(item.get_content().decode("utf-8")) - ) - - text = "\n".join(text_list) - return text diff --git a/scripts/parser/file/html_parser.py b/scripts/parser/file/html_parser.py deleted file mode 100644 index 71c95fef..00000000 --- a/scripts/parser/file/html_parser.py +++ /dev/null @@ -1,83 +0,0 @@ -"""HTML parser. - -Contains parser for html files. - -""" -import re -from pathlib import Path -from typing import Dict, Union - -from parser.file.base_parser import BaseParser - - -class HTMLParser(BaseParser): - """HTML parser.""" - - def _init_parser(self) -> Dict: - """Init parser.""" - return {} - - def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, list[str]]: - """Parse file. - - Returns: - Union[str, List[str]]: a string or a List of strings. - """ - try: - from unstructured.partition.html import partition_html - from unstructured.staging.base import convert_to_isd - from unstructured.cleaners.core import clean - except ImportError: - raise ValueError("unstructured package is required to parse HTML files.") - - # Using the unstructured library to convert the html to isd format - # isd sample : isd = [ - # {"text": "My Title", "type": "Title"}, - # {"text": "My Narrative", "type": "NarrativeText"} - # ] - with open(file, "r", encoding="utf-8") as fp: - elements = partition_html(file=fp) - isd = convert_to_isd(elements) - - # Removing non ascii charactwers from isd_el['text'] - for isd_el in isd: - isd_el['text'] = isd_el['text'].encode("ascii", "ignore").decode() - - # Removing all the \n characters from isd_el['text'] using regex and replace with single space - # Removing all the extra spaces from isd_el['text'] using regex and replace with single space - for isd_el in isd: - isd_el['text'] = re.sub(r'\n', ' ', isd_el['text'], flags=re.MULTILINE | re.DOTALL) - isd_el['text'] = re.sub(r"\s{2,}", " ", isd_el['text'], flags=re.MULTILINE | re.DOTALL) - - # more cleaning: extra_whitespaces, dashes, bullets, trailing_punctuation - for isd_el in isd: - clean(isd_el['text'], extra_whitespace=True, dashes=True, bullets=True, trailing_punctuation=True) - - # Creating a list of all the indexes of isd_el['type'] = 'Title' - title_indexes = [i for i, isd_el in enumerate(isd) if isd_el['type'] == 'Title'] - - # Creating 'Chunks' - List of lists of strings - # each list starting with isd_el['type'] = 'Title' and all the data till the next 'Title' - # Each Chunk can be thought of as an individual set of data, which can be sent to the model - # Where Each Title is grouped together with the data under it - - Chunks = [[]] - final_chunks = list(list()) - - for i, isd_el in enumerate(isd): - if i in title_indexes: - Chunks.append([]) - Chunks[-1].append(isd_el['text']) - - # Removing all the chunks with sum of length of all the strings in the chunk < 25 - # TODO: This value can be a user defined variable - for chunk in Chunks: - # sum of length of all the strings in the chunk - sum = 0 - sum += len(str(chunk)) - if sum < 25: - Chunks.remove(chunk) - else: - # appending all the approved chunks to final_chunks as a single string - final_chunks.append(" ".join([str(item) for item in chunk])) - return final_chunks diff --git a/scripts/parser/file/markdown_parser.py b/scripts/parser/file/markdown_parser.py deleted file mode 100644 index 2b4223df..00000000 --- a/scripts/parser/file/markdown_parser.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Markdown parser. - -Contains parser for md files. - -""" -import re -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Union, cast - -import tiktoken -from parser.file.base_parser import BaseParser - - -class MarkdownParser(BaseParser): - """Markdown parser. - - Extract text from markdown files. - Returns dictionary with keys as headers and values as the text between headers. - - """ - - def __init__( - self, - *args: Any, - remove_hyperlinks: bool = True, - remove_images: bool = True, - max_tokens: int = 2048, - # remove_tables: bool = True, - **kwargs: Any, - ) -> None: - """Init params.""" - super().__init__(*args, **kwargs) - self._remove_hyperlinks = remove_hyperlinks - self._remove_images = remove_images - self._max_tokens = max_tokens - # self._remove_tables = remove_tables - - def tups_chunk_append(self, tups: List[Tuple[Optional[str], str]], current_header: Optional[str], - current_text: str): - """Append to tups chunk.""" - num_tokens = len(tiktoken.get_encoding("cl100k_base").encode(current_text)) - if num_tokens > self._max_tokens: - chunks = [current_text[i:i + self._max_tokens] for i in range(0, len(current_text), self._max_tokens)] - for chunk in chunks: - tups.append((current_header, chunk)) - else: - tups.append((current_header, current_text)) - return tups - - def markdown_to_tups(self, markdown_text: str) -> List[Tuple[Optional[str], str]]: - """Convert a markdown file to a dictionary. - - The keys are the headers and the values are the text under each header. - - """ - markdown_tups: List[Tuple[Optional[str], str]] = [] - lines = markdown_text.split("\n") - - current_header = None - current_text = "" - - for line in lines: - header_match = re.match(r"^#+\s", line) - if header_match: - if current_header is not None: - if current_text == "" or None: - continue - markdown_tups = self.tups_chunk_append(markdown_tups, current_header, current_text) - - current_header = line - current_text = "" - else: - current_text += line + "\n" - markdown_tups = self.tups_chunk_append(markdown_tups, current_header, current_text) - - if current_header is not None: - # pass linting, assert keys are defined - markdown_tups = [ - (re.sub(r"#", "", cast(str, key)).strip(), re.sub(r"<.*?>", "", value)) - for key, value in markdown_tups - ] - else: - markdown_tups = [ - (key, re.sub("\n", "", value)) for key, value in markdown_tups - ] - - return markdown_tups - - def remove_images(self, content: str) -> str: - """Get a dictionary of a markdown file from its path.""" - pattern = r"!{1}\[\[(.*)\]\]" - content = re.sub(pattern, "", content) - return content - - # def remove_tables(self, content: str) -> List[List[str]]: - # """Convert markdown tables to nested lists.""" - # table_rows_pattern = r"((\r?\n){2}|^)([^\r\n]*\|[^\r\n]*(\r?\n)?)+(?=(\r?\n){2}|$)" - # table_cells_pattern = r"([^\|\r\n]*)\|" - # - # table_rows = re.findall(table_rows_pattern, content, re.MULTILINE) - # table_lists = [] - # for row in table_rows: - # cells = re.findall(table_cells_pattern, row[2]) - # cells = [cell.strip() for cell in cells if cell.strip()] - # table_lists.append(cells) - # return str(table_lists) - - def remove_hyperlinks(self, content: str) -> str: - """Get a dictionary of a markdown file from its path.""" - pattern = r"\[(.*?)\]\((.*?)\)" - content = re.sub(pattern, r"\1", content) - return content - - def _init_parser(self) -> Dict: - """Initialize the parser with the config.""" - return {} - - def parse_tups( - self, filepath: Path, errors: str = "ignore" - ) -> List[Tuple[Optional[str], str]]: - """Parse file into tuples.""" - with open(filepath, "r", encoding='utf8') as f: - try: - content = f.read() - except (Exception,) as e: - print(f'Error a file: "{filepath}"') - raise e - if self._remove_hyperlinks: - content = self.remove_hyperlinks(content) - if self._remove_images: - content = self.remove_images(content) - # if self._remove_tables: - # content = self.remove_tables(content) - markdown_tups = self.markdown_to_tups(content) - return markdown_tups - - def parse_file( - self, filepath: Path, errors: str = "ignore" - ) -> Union[str, List[str]]: - """Parse file into string.""" - tups = self.parse_tups(filepath, errors=errors) - results = [] - # TODO: don't include headers right now - for header, value in tups: - if header is None: - results.append(value) - else: - results.append(f"\n\n{header}\n{value}") - return results diff --git a/scripts/parser/file/openapi3_parser.py b/scripts/parser/file/openapi3_parser.py deleted file mode 100644 index 4fd1ffaf..00000000 --- a/scripts/parser/file/openapi3_parser.py +++ /dev/null @@ -1,51 +0,0 @@ -from urllib.parse import urlparse - -from openapi_parser import parse - -try: - from scripts.parser.file.base_parser import BaseParser -except ModuleNotFoundError: - from base_parser import BaseParser - - -class OpenAPI3Parser(BaseParser): - def init_parser(self) -> None: - return super().init_parser() - - def get_base_urls(self, urls): - base_urls = [] - for i in urls: - parsed_url = urlparse(i) - base_url = parsed_url.scheme + "://" + parsed_url.netloc - if base_url not in base_urls: - base_urls.append(base_url) - return base_urls - - def get_info_from_paths(self, path): - info = "" - if path.operations: - for operation in path.operations: - info += ( - f"\n{operation.method.value}=" - f"{operation.responses[0].description}" - ) - return info - - def parse_file(self, file_path): - data = parse(file_path) - results = "" - base_urls = self.get_base_urls(link.url for link in data.servers) - base_urls = ",".join([base_url for base_url in base_urls]) - results += f"Base URL:{base_urls}\n" - i = 1 - for path in data.paths: - info = self.get_info_from_paths(path) - results += ( - f"Path{i}: {path.url}\n" - f"description: {path.description}\n" - f"parameters: {path.parameters}\nmethods: {info}\n" - ) - i += 1 - with open("results.txt", "w") as f: - f.write(results) - return results diff --git a/scripts/parser/file/rst_parser.py b/scripts/parser/file/rst_parser.py deleted file mode 100644 index 887571b5..00000000 --- a/scripts/parser/file/rst_parser.py +++ /dev/null @@ -1,173 +0,0 @@ -"""reStructuredText parser. - -Contains parser for md files. - -""" -import re -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Union - -from parser.file.base_parser import BaseParser - - -class RstParser(BaseParser): - """reStructuredText parser. - - Extract text from .rst files. - Returns dictionary with keys as headers and values as the text between headers. - - """ - - def __init__( - self, - *args: Any, - remove_hyperlinks: bool = True, - remove_images: bool = True, - remove_table_excess: bool = True, - remove_interpreters: bool = True, - remove_directives: bool = True, - remove_whitespaces_excess: bool = True, - # Be careful with remove_characters_excess, might cause data loss - remove_characters_excess: bool = True, - **kwargs: Any, - ) -> None: - """Init params.""" - super().__init__(*args, **kwargs) - self._remove_hyperlinks = remove_hyperlinks - self._remove_images = remove_images - self._remove_table_excess = remove_table_excess - self._remove_interpreters = remove_interpreters - self._remove_directives = remove_directives - self._remove_whitespaces_excess = remove_whitespaces_excess - self._remove_characters_excess = remove_characters_excess - - def rst_to_tups(self, rst_text: str) -> List[Tuple[Optional[str], str]]: - """Convert a reStructuredText file to a dictionary. - - The keys are the headers and the values are the text under each header. - - """ - rst_tups: List[Tuple[Optional[str], str]] = [] - lines = rst_text.split("\n") - - current_header = None - current_text = "" - - for i, line in enumerate(lines): - header_match = re.match(r"^[^\S\n]*[-=]+[^\S\n]*$", line) - if header_match and i > 0 and ( - len(lines[i - 1].strip()) == len(header_match.group().strip()) or lines[i - 2] == lines[i - 2]): - if current_header is not None: - if current_text == "" or None: - continue - # removes the next heading from current Document - if current_text.endswith(lines[i - 1] + "\n"): - current_text = current_text[:len(current_text) - len(lines[i - 1] + "\n")] - rst_tups.append((current_header, current_text)) - - current_header = lines[i - 1] - current_text = "" - else: - current_text += line + "\n" - - rst_tups.append((current_header, current_text)) - - # TODO: Format for rst - # - # if current_header is not None: - # # pass linting, assert keys are defined - # rst_tups = [ - # (re.sub(r"#", "", cast(str, key)).strip(), re.sub(r"<.*?>", "", value)) - # for key, value in rst_tups - # ] - # else: - # rst_tups = [ - # (key, re.sub("\n", "", value)) for key, value in rst_tups - # ] - - if current_header is None: - rst_tups = [ - (key, re.sub("\n", "", value)) for key, value in rst_tups - ] - return rst_tups - - def remove_images(self, content: str) -> str: - pattern = r"\.\. image:: (.*)" - content = re.sub(pattern, "", content) - return content - - def remove_hyperlinks(self, content: str) -> str: - pattern = r"`(.*?) <(.*?)>`_" - content = re.sub(pattern, r"\1", content) - return content - - def remove_directives(self, content: str) -> str: - """Removes reStructuredText Directives""" - pattern = r"`\.\.([^:]+)::" - content = re.sub(pattern, "", content) - return content - - def remove_interpreters(self, content: str) -> str: - """Removes reStructuredText Interpreted Text Roles""" - pattern = r":(\w+):" - content = re.sub(pattern, "", content) - return content - - def remove_table_excess(self, content: str) -> str: - """Pattern to remove grid table separators""" - pattern = r"^\+[-]+\+[-]+\+$" - content = re.sub(pattern, "", content, flags=re.MULTILINE) - return content - - def remove_whitespaces_excess(self, content: List[Tuple[str, Any]]) -> List[Tuple[str, Any]]: - """Pattern to match 2 or more consecutive whitespaces""" - pattern = r"\s{2,}" - content = [(key, re.sub(pattern, " ", value)) for key, value in content] - return content - - def remove_characters_excess(self, content: List[Tuple[str, Any]]) -> List[Tuple[str, Any]]: - """Pattern to match 2 or more consecutive characters""" - pattern = r"(\S)\1{2,}" - content = [(key, re.sub(pattern, r"\1\1\1", value, flags=re.MULTILINE)) for key, value in content] - return content - - def _init_parser(self) -> Dict: - """Initialize the parser with the config.""" - return {} - - def parse_tups( - self, filepath: Path, errors: str = "ignore" - ) -> List[Tuple[Optional[str], str]]: - """Parse file into tuples.""" - with open(filepath, "r") as f: - content = f.read() - if self._remove_hyperlinks: - content = self.remove_hyperlinks(content) - if self._remove_images: - content = self.remove_images(content) - if self._remove_table_excess: - content = self.remove_table_excess(content) - if self._remove_directives: - content = self.remove_directives(content) - if self._remove_interpreters: - content = self.remove_interpreters(content) - rst_tups = self.rst_to_tups(content) - if self._remove_whitespaces_excess: - rst_tups = self.remove_whitespaces_excess(rst_tups) - if self._remove_characters_excess: - rst_tups = self.remove_characters_excess(rst_tups) - return rst_tups - - def parse_file( - self, filepath: Path, errors: str = "ignore" - ) -> Union[str, List[str]]: - """Parse file into string.""" - tups = self.parse_tups(filepath, errors=errors) - results = [] - # TODO: don't include headers right now - for header, value in tups: - if header is None: - results.append(value) - else: - results.append(f"\n\n{header}\n{value}") - return results diff --git a/scripts/parser/file/tabular_parser.py b/scripts/parser/file/tabular_parser.py deleted file mode 100644 index d7c6402a..00000000 --- a/scripts/parser/file/tabular_parser.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Tabular parser. - -Contains parsers for tabular data files. - -""" -from pathlib import Path -from typing import Any, Dict, List, Union - -from parser.file.base_parser import BaseParser - - -class CSVParser(BaseParser): - """CSV parser. - - Args: - concat_rows (bool): whether to concatenate all rows into one document. - If set to False, a Document will be created for each row. - True by default. - - """ - - def __init__(self, *args: Any, concat_rows: bool = True, **kwargs: Any) -> None: - """Init params.""" - super().__init__(*args, **kwargs) - self._concat_rows = concat_rows - - def _init_parser(self) -> Dict: - """Init parser.""" - return {} - - def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, List[str]]: - """Parse file. - - Returns: - Union[str, List[str]]: a string or a List of strings. - - """ - try: - import csv - except ImportError: - raise ValueError("csv module is required to read CSV files.") - text_list = [] - with open(file, "r") as fp: - csv_reader = csv.reader(fp) - for row in csv_reader: - text_list.append(", ".join(row)) - if self._concat_rows: - return "\n".join(text_list) - else: - return text_list - - -class PandasCSVParser(BaseParser): - r"""Pandas-based CSV parser. - - Parses CSVs using the separator detection from Pandas `read_csv`function. - If special parameters are required, use the `pandas_config` dict. - - Args: - concat_rows (bool): whether to concatenate all rows into one document. - If set to False, a Document will be created for each row. - True by default. - - col_joiner (str): Separator to use for joining cols per row. - Set to ", " by default. - - row_joiner (str): Separator to use for joining each row. - Only used when `concat_rows=True`. - Set to "\n" by default. - - pandas_config (dict): Options for the `pandas.read_csv` function call. - Refer to https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html - for more information. - Set to empty dict by default, this means pandas will try to figure - out the separators, table head, etc. on its own. - - """ - - def __init__( - self, - *args: Any, - concat_rows: bool = True, - col_joiner: str = ", ", - row_joiner: str = "\n", - pandas_config: dict = {}, - **kwargs: Any - ) -> None: - """Init params.""" - super().__init__(*args, **kwargs) - self._concat_rows = concat_rows - self._col_joiner = col_joiner - self._row_joiner = row_joiner - self._pandas_config = pandas_config - - def _init_parser(self) -> Dict: - """Init parser.""" - return {} - - def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, List[str]]: - """Parse file.""" - try: - import pandas as pd - except ImportError: - raise ValueError("pandas module is required to read CSV files.") - - df = pd.read_csv(file, **self._pandas_config) - - text_list = df.apply( - lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1 - ).tolist() - - if self._concat_rows: - return (self._row_joiner).join(text_list) - else: - return text_list diff --git a/scripts/parser/java2doc.py b/scripts/parser/java2doc.py deleted file mode 100644 index 2a8bfa3a..00000000 --- a/scripts/parser/java2doc.py +++ /dev/null @@ -1,66 +0,0 @@ -import os - -import javalang - - -def find_files(directory): - files_list = [] - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith('.java'): - files_list.append(os.path.join(root, file)) - return files_list - - -def extract_functions(file_path): - with open(file_path, "r") as file: - java_code = file.read() - methods = {} - tree = javalang.parse.parse(java_code) - for _, node in tree.filter(javalang.tree.MethodDeclaration): - method_name = node.name - start_line = node.position.line - 1 - end_line = start_line - brace_count = 0 - for line in java_code.splitlines()[start_line:]: - end_line += 1 - brace_count += line.count("{") - line.count("}") - if brace_count == 0: - break - method_source_code = "\n".join(java_code.splitlines()[start_line:end_line]) - methods[method_name] = method_source_code - return methods - - -def extract_classes(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - classes = {} - tree = javalang.parse.parse(source_code) - for class_decl in tree.types: - class_name = class_decl.name - declarations = [] - methods = [] - for field_decl in class_decl.fields: - field_name = field_decl.declarators[0].name - field_type = field_decl.type.name - declarations.append(f"{field_type} {field_name}") - for method_decl in class_decl.methods: - methods.append(method_decl.name) - class_string = "Declarations: " + ", ".join(declarations) + "\n Method name: " + ", ".join(methods) - classes[class_name] = class_string - return classes - - -def extract_functions_and_classes(directory): - files = find_files(directory) - functions_dict = {} - classes_dict = {} - for file in files: - functions = extract_functions(file) - if functions: - functions_dict[file] = functions - classes = extract_classes(file) - if classes: - classes_dict[file] = classes - return functions_dict, classes_dict diff --git a/scripts/parser/js2doc.py b/scripts/parser/js2doc.py deleted file mode 100644 index 6dc44812..00000000 --- a/scripts/parser/js2doc.py +++ /dev/null @@ -1,70 +0,0 @@ -import os - -import escodegen -import esprima - - -def find_files(directory): - files_list = [] - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith('.js'): - files_list.append(os.path.join(root, file)) - return files_list - - -def extract_functions(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - functions = {} - tree = esprima.parseScript(source_code) - for node in tree.body: - if node.type == 'FunctionDeclaration': - func_name = node.id.name if node.id else '' - functions[func_name] = escodegen.generate(node) - elif node.type == 'VariableDeclaration': - for declaration in node.declarations: - if declaration.init and declaration.init.type == 'FunctionExpression': - func_name = declaration.id.name if declaration.id else '' - functions[func_name] = escodegen.generate(declaration.init) - elif node.type == 'ClassDeclaration': - for subnode in node.body.body: - if subnode.type == 'MethodDefinition': - func_name = subnode.key.name - functions[func_name] = escodegen.generate(subnode.value) - elif subnode.type == 'VariableDeclaration': - for declaration in subnode.declarations: - if declaration.init and declaration.init.type == 'FunctionExpression': - func_name = declaration.id.name if declaration.id else '' - functions[func_name] = escodegen.generate(declaration.init) - return functions - - -def extract_classes(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - classes = {} - tree = esprima.parseScript(source_code) - for node in tree.body: - if node.type == 'ClassDeclaration': - class_name = node.id.name - function_names = [] - for subnode in node.body.body: - if subnode.type == 'MethodDefinition': - function_names.append(subnode.key.name) - classes[class_name] = ", ".join(function_names) - return classes - - -def extract_functions_and_classes(directory): - files = find_files(directory) - functions_dict = {} - classes_dict = {} - for file in files: - functions = extract_functions(file) - if functions: - functions_dict[file] = functions - classes = extract_classes(file) - if classes: - classes_dict[file] = classes - return functions_dict, classes_dict diff --git a/scripts/parser/open_ai_func.py b/scripts/parser/open_ai_func.py deleted file mode 100644 index 91b9f692..00000000 --- a/scripts/parser/open_ai_func.py +++ /dev/null @@ -1,100 +0,0 @@ -import os - -import tiktoken -from langchain_openai import OpenAIEmbeddings -from langchain_community.vectorstores import FAISS -from retry import retry - - -# from langchain.embeddings import HuggingFaceEmbeddings -# from langchain.embeddings import HuggingFaceInstructEmbeddings -# from langchain.embeddings import CohereEmbeddings - - -def num_tokens_from_string(string: str, encoding_name: str) -> tuple[int, float]: - # Function to convert string to tokens and estimate user cost. - encoding = tiktoken.get_encoding(encoding_name) - num_tokens = len(encoding.encode(string)) - total_price = (num_tokens / 1000) * 0.0004 - return num_tokens, total_price - - -@retry(tries=10, delay=60) -def store_add_texts_with_retry(store, i): - store.add_texts([i.page_content], metadatas=[i.metadata]) - # store_pine.add_texts([i.page_content], metadatas=[i.metadata]) - - -def call_openai_api(docs, folder_name): - # Function to create a vector store from the documents and save it to disk. - - # create output folder if it doesn't exist - if not os.path.exists(f"outputs/{folder_name}"): - os.makedirs(f"outputs/{folder_name}") - - from tqdm import tqdm - - docs_test = [docs[0]] - # remove the first element from docs - docs.pop(0) - # cut first n docs if you want to restart - # docs = docs[:n] - c1 = 0 - # pinecone.init( - # api_key="", # find at app.pinecone.io - # environment="us-east1-gcp" # next to api key in console - # ) - # index_name = "pandas" - if ( # azure - os.environ.get("OPENAI_API_BASE") - and os.environ.get("OPENAI_API_VERSION") - and os.environ.get("AZURE_DEPLOYMENT_NAME") - and os.environ.get("AZURE_EMBEDDINGS_DEPLOYMENT_NAME") - ): - os.environ["OPENAI_API_TYPE"] = "azure" - openai_embeddings = OpenAIEmbeddings(model=os.environ.get("AZURE_EMBEDDINGS_DEPLOYMENT_NAME")) - else: - openai_embeddings = OpenAIEmbeddings() - store = FAISS.from_documents(docs_test, openai_embeddings) - # store_pine = Pinecone.from_documents(docs_test, OpenAIEmbeddings(), index_name=index_name) - - # Uncomment for MPNet embeddings - # model_name = "sentence-transformers/all-mpnet-base-v2" - # hf = HuggingFaceEmbeddings(model_name=model_name) - # store = FAISS.from_documents(docs_test, hf) - for i in tqdm( - docs, desc="Embedding 🦖", unit="docs", total=len(docs), bar_format="{l_bar}{bar}| Time Left: {remaining}" - ): - try: - store_add_texts_with_retry(store, i) - except Exception as e: - print(e) - print("Error on ", i) - print("Saving progress") - print(f"stopped at {c1} out of {len(docs)}") - store.save_local(f"outputs/{folder_name}") - break - c1 += 1 - store.save_local(f"outputs/{folder_name}") - - -def get_user_permission(docs, folder_name): - # Function to ask user permission to call the OpenAI api and spend their OpenAI funds. - # Here we convert the docs list to a string and calculate the number of OpenAI tokens the string represents. - # docs_content = (" ".join(docs)) - docs_content = "" - for doc in docs: - docs_content += doc.page_content - - tokens, total_price = num_tokens_from_string(string=docs_content, encoding_name="cl100k_base") - # Here we print the number of tokens and the approx user cost with some visually appealing formatting. - print(f"Number of Tokens = {format(tokens, ',d')}") - print(f"Approx Cost = ${format(total_price, ',.2f')}") - # Here we check for user permission before calling the API. - user_input = input("Price Okay? (Y/N) \n").lower() - if user_input == "y": - call_openai_api(docs, folder_name) - elif user_input == "": - call_openai_api(docs, folder_name) - else: - print("The API was not called. No money was spent.") diff --git a/scripts/parser/py2doc.py b/scripts/parser/py2doc.py deleted file mode 100644 index d8f39885..00000000 --- a/scripts/parser/py2doc.py +++ /dev/null @@ -1,121 +0,0 @@ -import ast -import os -from pathlib import Path - -import tiktoken -from langchain_community.llms import OpenAI -from langchain.prompts import PromptTemplate - - -def find_files(directory): - files_list = [] - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith('.py'): - files_list.append(os.path.join(root, file)) - return files_list - - -def extract_functions(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - functions = {} - tree = ast.parse(source_code) - for node in ast.walk(tree): - if isinstance(node, ast.FunctionDef): - func_name = node.name - func_def = ast.get_source_segment(source_code, node) - functions[func_name] = func_def - return functions - - -def extract_classes(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - classes = {} - tree = ast.parse(source_code) - for node in ast.walk(tree): - if isinstance(node, ast.ClassDef): - class_name = node.name - function_names = [] - for subnode in ast.walk(node): - if isinstance(subnode, ast.FunctionDef): - function_names.append(subnode.name) - classes[class_name] = ", ".join(function_names) - return classes - - -def extract_functions_and_classes(directory): - files = find_files(directory) - functions_dict = {} - classes_dict = {} - for file in files: - functions = extract_functions(file) - if functions: - functions_dict[file] = functions - classes = extract_classes(file) - if classes: - classes_dict[file] = classes - return functions_dict, classes_dict - - -def parse_functions(functions_dict, formats, dir): - c1 = len(functions_dict) - for i, (source, functions) in enumerate(functions_dict.items(), start=1): - print(f"Processing file {i}/{c1}") - source_w = source.replace(dir + "/", "").replace("." + formats, ".md") - subfolders = "/".join(source_w.split("/")[:-1]) - Path(f"outputs/{subfolders}").mkdir(parents=True, exist_ok=True) - for j, (name, function) in enumerate(functions.items(), start=1): - print(f"Processing function {j}/{len(functions)}") - prompt = PromptTemplate( - input_variables=["code"], - template="Code: \n{code}, \nDocumentation: ", - ) - llm = OpenAI(temperature=0) - response = llm(prompt.format(code=function)) - mode = "a" if Path(f"outputs/{source_w}").exists() else "w" - with open(f"outputs/{source_w}", mode) as f: - f.write( - f"\n\n# Function name: {name} \n\nFunction: \n```\n{function}\n```, \nDocumentation: \n{response}") - - -def parse_classes(classes_dict, formats, dir): - c1 = len(classes_dict) - for i, (source, classes) in enumerate(classes_dict.items()): - print(f"Processing file {i + 1}/{c1}") - source_w = source.replace(dir + "/", "").replace("." + formats, ".md") - subfolders = "/".join(source_w.split("/")[:-1]) - Path(f"outputs/{subfolders}").mkdir(parents=True, exist_ok=True) - for name, function_names in classes.items(): - print(f"Processing Class {i + 1}/{c1}") - prompt = PromptTemplate( - input_variables=["class_name", "functions_names"], - template="Class name: {class_name} \nFunctions: {functions_names}, \nDocumentation: ", - ) - llm = OpenAI(temperature=0) - response = llm(prompt.format(class_name=name, functions_names=function_names)) - - with open(f"outputs/{source_w}", "a" if Path(f"outputs/{source_w}").exists() else "w") as f: - f.write(f"\n\n# Class name: {name} \n\nFunctions: \n{function_names}, \nDocumentation: \n{response}") - - -def transform_to_docs(functions_dict, classes_dict, formats, dir): - docs_content = ''.join([str(key) + str(value) for key, value in functions_dict.items()]) - docs_content += ''.join([str(key) + str(value) for key, value in classes_dict.items()]) - - num_tokens = len(tiktoken.get_encoding("cl100k_base").encode(docs_content)) - total_price = ((num_tokens / 1000) * 0.02) - - print(f"Number of Tokens = {num_tokens:,d}") - print(f"Approx Cost = ${total_price:,.2f}") - - user_input = input("Price Okay? (Y/N)\n").lower() - if user_input == "y" or user_input == "": - if not Path("outputs").exists(): - Path("outputs").mkdir() - parse_functions(functions_dict, formats, dir) - parse_classes(classes_dict, formats, dir) - print("All done!") - else: - print("The API was not called. No money was spent.") diff --git a/scripts/parser/schema/__init__.py b/scripts/parser/schema/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/parser/schema/base.py b/scripts/parser/schema/base.py deleted file mode 100644 index 3dafda1a..00000000 --- a/scripts/parser/schema/base.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Base schema for readers.""" -from dataclasses import dataclass - -from langchain.docstore.document import Document as LCDocument -from parser.schema.schema import BaseDocument - - -@dataclass -class Document(BaseDocument): - """Generic interface for a data document. - - This document connects to data sources. - - """ - - def __post_init__(self) -> None: - """Post init.""" - if self.text is None: - raise ValueError("text field not set.") - - @classmethod - def get_type(cls) -> str: - """Get Document type.""" - return "Document" - - def to_langchain_format(self) -> LCDocument: - """Convert struct to LangChain document format.""" - metadata = self.extra_info or {} - return LCDocument(page_content=self.text, metadata=metadata) - - @classmethod - def from_langchain_format(cls, doc: LCDocument) -> "Document": - """Convert struct from LangChain document format.""" - return cls(text=doc.page_content, extra_info=doc.metadata) diff --git a/scripts/parser/schema/schema.py b/scripts/parser/schema/schema.py deleted file mode 100644 index ec467e5a..00000000 --- a/scripts/parser/schema/schema.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Base schema for data structures.""" -from abc import abstractmethod -from dataclasses import dataclass -from typing import Any, Dict, List, Optional - -from dataclasses_json import DataClassJsonMixin - - -@dataclass -class BaseDocument(DataClassJsonMixin): - """Base document. - - Generic abstract interfaces that captures both index structs - as well as documents. - - """ - - # TODO: consolidate fields from Document/IndexStruct into base class - text: Optional[str] = None - doc_id: Optional[str] = None - embedding: Optional[List[float]] = None - - # extra fields - extra_info: Optional[Dict[str, Any]] = None - - @classmethod - @abstractmethod - def get_type(cls) -> str: - """Get Document type.""" - - def get_text(self) -> str: - """Get text.""" - if self.text is None: - raise ValueError("text field not set.") - return self.text - - def get_doc_id(self) -> str: - """Get doc_id.""" - if self.doc_id is None: - raise ValueError("doc_id not set.") - return self.doc_id - - @property - def is_doc_id_none(self) -> bool: - """Check if doc_id is None.""" - return self.doc_id is None - - def get_embedding(self) -> List[float]: - """Get embedding. - - Errors if embedding is None. - - """ - if self.embedding is None: - raise ValueError("embedding not set.") - return self.embedding - - @property - def extra_info_str(self) -> Optional[str]: - """Extra info string.""" - if self.extra_info is None: - return None - - return "\n".join([f"{k}: {str(v)}" for k, v in self.extra_info.items()]) diff --git a/scripts/parser/token_func.py b/scripts/parser/token_func.py deleted file mode 100644 index 98ab5470..00000000 --- a/scripts/parser/token_func.py +++ /dev/null @@ -1,76 +0,0 @@ -import re -from math import ceil -from typing import List - -import tiktoken -from parser.schema.base import Document - -def separate_header_and_body(text): - header_pattern = r"^(.*?\n){3}" - match = re.match(header_pattern, text) - header = match.group(0) - body = text[len(header):] - return header, body - - -def group_documents(documents: List[Document], min_tokens: int, max_tokens: int) -> List[Document]: - docs = [] - current_group = None - - for doc in documents: - doc_len = len(tiktoken.get_encoding("cl100k_base").encode(doc.text)) - - if current_group is None: - current_group = Document(text=doc.text, doc_id=doc.doc_id, embedding=doc.embedding, - extra_info=doc.extra_info) - elif len(tiktoken.get_encoding("cl100k_base").encode( - current_group.text)) + doc_len < max_tokens and doc_len < min_tokens: - current_group.text += " " + doc.text - else: - docs.append(current_group) - current_group = Document(text=doc.text, doc_id=doc.doc_id, embedding=doc.embedding, - extra_info=doc.extra_info) - - if current_group is not None: - docs.append(current_group) - - return docs - - -def split_documents(documents: List[Document], max_tokens: int) -> List[Document]: - docs = [] - for doc in documents: - token_length = len(tiktoken.get_encoding("cl100k_base").encode(doc.text)) - if token_length <= max_tokens: - docs.append(doc) - else: - header, body = separate_header_and_body(doc.text) - if len(tiktoken.get_encoding("cl100k_base").encode(header)) > max_tokens: - body = doc.text - header = "" - num_body_parts = ceil(token_length / max_tokens) - part_length = ceil(len(body) / num_body_parts) - body_parts = [body[i:i + part_length] for i in range(0, len(body), part_length)] - for i, body_part in enumerate(body_parts): - new_doc = Document(text=header + body_part.strip(), - doc_id=f"{doc.doc_id}-{i}", - embedding=doc.embedding, - extra_info=doc.extra_info) - docs.append(new_doc) - return docs - - -def group_split(documents: List[Document], max_tokens: int = 2000, min_tokens: int = 150, token_check: bool = True): - if not token_check: - return documents - print("Grouping small documents") - try: - documents = group_documents(documents=documents, min_tokens=min_tokens, max_tokens=max_tokens) - except Exception: - print("Grouping failed, try running without token_check") - print("Separating large documents") - try: - documents = split_documents(documents=documents, max_tokens=max_tokens) - except Exception: - print("Grouping failed, try running without token_check") - return documents diff --git a/scripts/requirements.txt b/scripts/requirements.txt deleted file mode 100644 index d90af2c3..00000000 --- a/scripts/requirements.txt +++ /dev/null @@ -1,22 +0,0 @@ -dataclasses_json==0.6.3 -docx2txt==0.8 -EbookLib==0.18 -escodegen==1.0.11 -esprima==4.0.1 -faiss_cpu==1.7.4 -html2text==2020.1.16 -javalang==0.13.0 -langchain==0.2.10 -langchain_community==0.2.9 -langchain-openai==0.0.5 -nltk==3.9 -openapi3_parser==1.1.16 -pandas==2.2.0 -PyPDF2==3.0.1 -python-dotenv==1.0.1 -retry==0.9.2 -Sphinx==7.2.6 -tiktoken==0.5.2 -tqdm==4.66.3 -typer==0.9.0 -unstructured==0.12.2 diff --git a/setup.sh b/setup.sh index 7980461b..7775e24e 100755 --- a/setup.sh +++ b/setup.sh @@ -1,14 +1,79 @@ #!/bin/bash -# Function to prompt the user for their choice -prompt_user() { - echo "Do you want to:" - echo "1. Use DocsGPT public API (simple and free)" - echo "2. Download the language model locally (12GB)" - echo "3. Use the OpenAI API (requires an API key)" - read -p "Enter your choice (1, 2 or 3): " choice +# Color codes +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +DEFAULT_FG='\033[39m' +RED='\033[0;31m' +NC='\033[0m' +BOLD='\033[1m' + +# Base Compose file (relative to script location) +COMPOSE_FILE="$(dirname "$(readlink -f "$0")")/deployment/docker-compose.yaml" +ENV_FILE="$(dirname "$(readlink -f "$0")")/.env" + +# Animation function +animate_dino() { + tput civis # Hide cursor + local dino_lines=( + " ######### " + " ############# " + " ##################" + " ####################" + " ######################" + " ####################### ######" + " ############################### " + " ################################## " + " ################ ############ " + " ################## ########## " + " ##################### ######## " + " ###################### ###### ### " + " ############ ########## #### ## " + " ############# ######### ##### " + " ############## ######### " + " ############## ########## " + "############ ####### " + " ###### ###### #### " + " ################ " + " ################# " + ) + + # Static DocsGPT text + local static_text=( + " ____ ____ ____ _____ " + " | _ \\ ___ ___ ___ / ___| _ \\_ _|" + " | | | |/ _ \\ / __/ __| | _| |_) || | " + " | |_| | (_) | (__\\__ \\ |_| | __/ | | " + " |____/ \\___/ \\___|___/\\____|_| |_| " + " " + ) + + # Print static text + clear + for line in "${static_text[@]}"; do + echo "$line" + done + + tput sc + + # Build-up animation + for i in "${!dino_lines[@]}"; do + tput rc + for ((j=0; j<=i; j++)); do + echo "${dino_lines[$j]}" + done + sleep 0.05 + done + + sleep 0.5 + + tput rc + tput ed + + tput cnorm } +# Check and start Docker function check_and_start_docker() { # Check if Docker is running if ! docker info > /dev/null 2>&1; then @@ -35,106 +100,390 @@ check_and_start_docker() { echo -n "." sleep 1 done - echo -ne "\rWaiting for Docker to start " # Reset to overwrite previous dots + echo -ne "\rWaiting for Docker to start " done echo -e "\nDocker has started!" fi } -# Function to handle the choice to download the model locally -download_locally() { - echo "LLM_NAME=llama.cpp" > .env - echo "VITE_API_STREAMING=true" >> .env - echo "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" >> .env - echo "The .env file has been created with LLM_NAME set to llama.cpp." - - # Creating the directory if it does not exist - mkdir -p models - - # Downloading the model to the specific directory - echo "Downloading the model..." - # check if docsgpt-7b-f16.gguf does not exist - if [ ! -f models/docsgpt-7b-f16.gguf ]; then - echo "Downloading the model..." - wget -P models https://d3dg1063dc54p9.cloudfront.net/models/docsgpt-7b-f16.gguf - echo "Model downloaded to models directory." - else - echo "Model already exists." - fi - - # Call the function to check and start Docker if needed - check_and_start_docker - - docker-compose -f docker-compose-local.yaml build && docker-compose -f docker-compose-local.yaml up -d - #python -m venv venv - #source venv/bin/activate - pip install -r application/requirements.txt - pip install llama-cpp-python - pip install sentence-transformers - export LLM_NAME=llama.cpp - export EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2 - export FLASK_APP=application/app.py - export FLASK_DEBUG=true - export CELERY_BROKER_URL=redis://localhost:6379/0 - export CELERY_RESULT_BACKEND=redis://localhost:6379/1 - echo "The application is now running on http://localhost:5173" - echo "You can stop the application by running the following command:" - echo "Ctrl + C and then" - echo "Then pkill -f 'flask run' and then" - echo "docker-compose down" - flask run --host=0.0.0.0 --port=7091 & - celery -A application.app.celery worker -l INFO +# Function to prompt the user for the main menu choice +prompt_main_menu() { + echo -e "\n${DEFAULT_FG}${BOLD}Welcome to DocsGPT Setup!${NC}" + echo -e "${DEFAULT_FG}How would you like to proceed?${NC}" + echo -e "${YELLOW}1) Use DocsGPT Public API Endpoint (simple and free)${NC}" + echo -e "${YELLOW}2) Serve Local (with Ollama)${NC}" + echo -e "${YELLOW}3) Connect Local Inference Engine${NC}" + echo -e "${YELLOW}4) Connect Cloud API Provider${NC}" + echo + read -p "$(echo -e "${DEFAULT_FG}Choose option (1-4): ${NC}")" main_choice } -# Function to handle the choice to use the OpenAI API -use_openai() { - read -p "Please enter your OpenAI API key: " api_key - echo "API_KEY=$api_key" > .env - echo "LLM_NAME=openai" >> .env - echo "VITE_API_STREAMING=true" >> .env - echo "The .env file has been created with API_KEY set to your provided key." - - # Call the function to check and start Docker if needed - check_and_start_docker - - docker-compose build && docker-compose up -d - - echo "The application will run on http://localhost:5173" - echo "You can stop the application by running the following command:" - echo "docker-compose down" +# Function to prompt for Local Inference Engine options +prompt_local_inference_engine_options() { + clear + echo -e "\n${DEFAULT_FG}${BOLD}Connect Local Inference Engine${NC}" + echo -e "${DEFAULT_FG}Choose your local inference engine:${NC}" + echo -e "${YELLOW}1) LLaMa.cpp${NC}" + echo -e "${YELLOW}2) Ollama${NC}" + echo -e "${YELLOW}3) Text Generation Inference (TGI)${NC}" + echo -e "${YELLOW}4) SGLang${NC}" + echo -e "${YELLOW}5) vLLM${NC}" + echo -e "${YELLOW}6) Aphrodite${NC}" + echo -e "${YELLOW}7) FriendliAI${NC}" + echo -e "${YELLOW}8) LMDeploy${NC}" + echo -e "${YELLOW}b) Back to Main Menu${NC}" + echo + read -p "$(echo -e "${DEFAULT_FG}Choose option (1-8, or b): ${NC}")" engine_choice } -use_docsgpt() { +# Function to prompt for Cloud API Provider options +prompt_cloud_api_provider_options() { + clear + echo -e "\n${DEFAULT_FG}${BOLD}Connect Cloud API Provider${NC}" + echo -e "${DEFAULT_FG}Choose your Cloud API Provider:${NC}" + echo -e "${YELLOW}1) OpenAI${NC}" + echo -e "${YELLOW}2) Google (Vertex AI, Gemini)${NC}" + echo -e "${YELLOW}3) Anthropic (Claude)${NC}" + echo -e "${YELLOW}4) Groq${NC}" + echo -e "${YELLOW}5) HuggingFace Inference API${NC}" + echo -e "${YELLOW}6) Azure OpenAI${NC}" + echo -e "${YELLOW}b) Back to Main Menu${NC}" + echo + read -p "$(echo -e "${DEFAULT_FG}Choose option (1-6, or b): ${NC}")" provider_choice +} + +# Function to prompt for Ollama CPU/GPU options +prompt_ollama_options() { + clear + echo -e "\n${DEFAULT_FG}${BOLD}Serve Local with Ollama${NC}" + echo -e "${DEFAULT_FG}Choose how to serve Ollama:${NC}" + echo -e "${YELLOW}1) CPU${NC}" + echo -e "${YELLOW}2) GPU${NC}" + echo -e "${YELLOW}b) Back to Main Menu${NC}" + echo + read -p "$(echo -e "${DEFAULT_FG}Choose option (1-2, or b): ${NC}")" ollama_choice +} + +# 1) Use DocsGPT Public API Endpoint (simple and free) +use_docs_public_api_endpoint() { + echo -e "\n${NC}Setting up DocsGPT Public API Endpoint...${NC}" echo "LLM_NAME=docsgpt" > .env echo "VITE_API_STREAMING=true" >> .env - echo "The .env file has been created with API_KEY set to your provided key." + echo -e "${GREEN}.env file configured for DocsGPT Public API.${NC}" - # Call the function to check and start Docker if needed check_and_start_docker - docker-compose build && docker-compose up -d + echo -e "\n${NC}Starting Docker Compose...${NC}" + docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" build && docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" up -d + docker_compose_status=$? # Capture exit status of docker compose - echo "The application will run on http://localhost:5173" - echo "You can stop the application by running the following command:" - echo "docker-compose down" + echo "Docker Compose Exit Status: $docker_compose_status" + + if [ "$docker_compose_status" -ne 0 ]; then + echo -e "\n${RED}${BOLD}Error starting Docker Compose. Please ensure Docker Compose is installed and in your PATH.${NC}" + echo -e "${RED}Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/${NC}" + exit 1 # Indicate failure and EXIT SCRIPT + fi + + echo -e "\n${GREEN}DocsGPT is now running on http://localhost:5173${NC}" + echo -e "${YELLOW}You can stop the application by running: docker compose -f \"${COMPOSE_FILE}\" down${NC}" } -# Prompt the user for their choice -prompt_user +# 2) Serve Local (with Ollama) +serve_local_ollama() { + local ollama_choice model_name + local docker_compose_file_suffix + local model_name_prompt + local default_model="llama3.2:1b" -# Handle the user's choice -case $choice in - 1) - use_docsgpt - ;; - 2) - download_locally - ;; - 3) - use_openai - ;; - *) - echo "Invalid choice. Please choose either 1 or 2." - ;; -esac + get_model_name_ollama() { + read -p "$(echo -e "${DEFAULT_FG}Enter Ollama Model Name (leave empty for default: ${default_model} (1.3GB)): ${NC}")" model_name_input + if [ -z "$model_name_input" ]; then + model_name="$default_model" # Set default model if input is empty + else + model_name="$model_name_input" # Use user-provided model name + fi + } + + + while true; do + clear + prompt_ollama_options + case "$ollama_choice" in + 1) # CPU + docker_compose_file_suffix="cpu" + get_model_name_ollama + break ;; + 2) # GPU + echo -e "\n${YELLOW}For this option to work correctly you need to have a supported GPU and configure Docker to utilize it.${NC}" + echo -e "${YELLOW}Refer to: https://hub.docker.com/r/ollama/ollama for more information.${NC}" + read -p "$(echo -e "${DEFAULT_FG}Continue with GPU setup? (y/b): ${NC}")" confirm_gpu + case "$confirm_gpu" in + y|Y) + docker_compose_file_suffix="gpu" + get_model_name_ollama + break ;; + b|B) clear; return ;; # Back to Main Menu + *) echo -e "\n${RED}Invalid choice. Please choose y or b.${NC}" ; sleep 1 ;; + esac + ;; + b|B) clear; return ;; # Back to Main Menu + *) echo -e "\n${RED}Invalid choice. Please choose 1-2, or b.${NC}" ; sleep 1 ;; + esac + done + + + echo -e "\n${NC}Configuring for Ollama ($(echo "$docker_compose_file_suffix" | tr '[:lower:]' '[:upper:]'))...${NC}" # Using tr for uppercase - more compatible + echo "API_KEY=xxxx" > .env # Placeholder API Key + echo "LLM_NAME=openai" >> .env + echo "MODEL_NAME=$model_name" >> .env + echo "VITE_API_STREAMING=true" >> .env + echo "OPENAI_BASE_URL=http://host.docker.internal:11434/v1" >> .env + echo "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" >> .env + echo -e "${GREEN}.env file configured for Ollama ($(echo "$docker_compose_file_suffix" | tr '[:lower:]' '[:upper:]')${NC}${GREEN}).${NC}" + echo -e "${YELLOW}Note: MODEL_NAME is set to '${BOLD}$model_name${NC}${YELLOW}'. You can change it later in the .env file.${NC}" + + + check_and_start_docker + local compose_files=( + -f "${COMPOSE_FILE}" + -f "$(dirname "${COMPOSE_FILE}")/optional/docker-compose.optional.ollama-${docker_compose_file_suffix}.yaml" + ) + + echo -e "\n${NC}Starting Docker Compose with Ollama (${docker_compose_file_suffix})...${NC}" + docker compose --env-file "${ENV_FILE}" "${compose_files[@]}" build + docker compose --env-file "${ENV_FILE}" "${compose_files[@]}" up -d + docker_compose_status=$? + + echo "Docker Compose Exit Status: $docker_compose_status" # Debug output + + if [ "$docker_compose_status" -ne 0 ]; then + echo -e "\n${RED}${BOLD}Error starting Docker Compose. Please ensure Docker Compose is installed and in your PATH.${NC}" + echo -e "${RED}Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/${NC}" + exit 1 # Indicate failure and EXIT SCRIPT + fi + + echo "Waiting for Ollama container to be ready..." + OLLAMA_READY=false + while ! $OLLAMA_READY; do + CONTAINER_STATUS=$(docker compose "${compose_files[@]}" ps --services --filter "status=running" --format '{{.Service}}') + if [[ "$CONTAINER_STATUS" == *"ollama"* ]]; then # Check if 'ollama' service is in running services + OLLAMA_READY=true + echo "Ollama container is running." + else + echo "Ollama container not yet ready, waiting..." + sleep 5 + fi + done + + echo "Pulling $model_name model for Ollama..." + docker compose --env-file "${ENV_FILE}" "${compose_files[@]}" exec -it ollama ollama pull "$model_name" + + + echo -e "\n${GREEN}DocsGPT is now running with Ollama (${docker_compose_file_suffix}) on http://localhost:5173${NC}" + printf -v compose_files_escaped "%q " "${compose_files[@]}" + echo -e "${YELLOW}You can stop the application by running: docker compose ${compose_files_escaped}down${NC}" +} + +# 3) Connect Local Inference Engine +connect_local_inference_engine() { + local engine_choice + local model_name_prompt model_name openai_base_url + + get_model_name() { + read -p "$(echo -e "${DEFAULT_FG}Enter Model Name (leave empty to set later as None): ${NC}")" model_name + if [ -z "$model_name" ]; then + model_name="None" + fi + } + + while true; do + clear + prompt_local_inference_engine_options + case "$engine_choice" in + 1) # LLaMa.cpp + engine_name="LLaMa.cpp" + openai_base_url="http://localhost:8000/v1" + get_model_name + break ;; + 2) # Ollama + engine_name="Ollama" + openai_base_url="http://localhost:11434/v1" + get_model_name + break ;; + 3) # TGI + engine_name="TGI" + openai_base_url="http://localhost:8080/v1" + get_model_name + break ;; + 4) # SGLang + engine_name="SGLang" + openai_base_url="http://localhost:30000/v1" + get_model_name + break ;; + 5) # vLLM + engine_name="vLLM" + openai_base_url="http://localhost:8000/v1" + get_model_name + break ;; + 6) # Aphrodite + engine_name="Aphrodite" + openai_base_url="http://localhost:2242/v1" + get_model_name + break ;; + 7) # FriendliAI + engine_name="FriendliAI" + openai_base_url="http://localhost:8997/v1" + get_model_name + break ;; + 8) # LMDeploy + engine_name="LMDeploy" + openai_base_url="http://localhost:23333/v1" + get_model_name + break ;; + b|B) clear; return ;; # Back to Main Menu + *) echo -e "\n${RED}Invalid choice. Please choose 1-8, or b.${NC}" ; sleep 1 ;; + esac + done + + echo -e "\n${NC}Configuring for Local Inference Engine: ${BOLD}${engine_name}...${NC}" + echo "API_KEY=None" > .env + echo "LLM_NAME=openai" >> .env + echo "MODEL_NAME=$model_name" >> .env + echo "VITE_API_STREAMING=true" >> .env + echo "OPENAI_BASE_URL=$openai_base_url" >> .env + echo "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" >> .env + echo -e "${GREEN}.env file configured for ${BOLD}${engine_name}${NC}${GREEN} with OpenAI API format.${NC}" + echo -e "${YELLOW}Note: MODEL_NAME is set to '${BOLD}$model_name${NC}${YELLOW}'. You can change it later in the .env file.${NC}" + + check_and_start_docker + + echo -e "\n${NC}Starting Docker Compose...${NC}" + docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" build && docker compose -f "${COMPOSE_FILE}" up -d + docker_compose_status=$? + + echo "Docker Compose Exit Status: $docker_compose_status" # Debug output + + if [ "$docker_compose_status" -ne 0 ]; then + echo -e "\n${RED}${BOLD}Error starting Docker Compose. Please ensure Docker Compose is installed and in your PATH.${NC}" + echo -e "${RED}Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/${NC}" + exit 1 # Indicate failure and EXIT SCRIPT + fi + + echo -e "\n${GREEN}DocsGPT is now configured to connect to ${BOLD}${engine_name}${NC}${GREEN} at ${BOLD}$openai_base_url${NC}" + echo -e "${YELLOW}Ensure your ${BOLD}${engine_name} inference server is running at that address${NC}" + echo -e "\n${GREEN}DocsGPT is running at http://localhost:5173${NC}" + echo -e "${YELLOW}You can stop the application by running: docker compose -f \"${COMPOSE_FILE}\" down${NC}" +} + + +# 4) Connect Cloud API Provider +connect_cloud_api_provider() { + local provider_choice api_key llm_name + local setup_result # Variable to store the return status + + get_api_key() { + echo -e "${YELLOW}Your API key will be stored locally in the .env file and will not be sent anywhere else${NC}" + read -p "$(echo -e "${DEFAULT_FG}Please enter your API key: ${NC}")" api_key + } + + while true; do + clear + prompt_cloud_api_provider_options + case "$provider_choice" in + 1) # OpenAI + provider_name="OpenAI" + llm_name="openai" + model_name="gpt-4o" + get_api_key + break ;; + 2) # Google + provider_name="Google (Vertex AI, Gemini)" + llm_name="google" + model_name="gemini-2.0-flash" + get_api_key + break ;; + 3) # Anthropic + provider_name="Anthropic (Claude)" + llm_name="anthropic" + model_name="claude-3-5-sonnet-latest" + get_api_key + break ;; + 4) # Groq + provider_name="Groq" + llm_name="groq" + model_name="llama-3.1-8b-instant" + get_api_key + break ;; + 5) # HuggingFace Inference API + provider_name="HuggingFace Inference API" + llm_name="huggingface" + model_name="meta-llama/Llama-3.1-8B-Instruct" + get_api_key + break ;; + 6) # Azure OpenAI + provider_name="Azure OpenAI" + llm_name="azure_openai" + model_name="gpt-4o" + get_api_key + break ;; + b|B) clear; return ;; # Clear screen and Back to Main Menu + *) echo -e "\n${RED}Invalid choice. Please choose 1-6, or b.${NC}" ; sleep 1 ;; + esac + done + + echo -e "\n${NC}Configuring for Cloud API Provider: ${BOLD}${provider_name}...${NC}" + echo "API_KEY=$api_key" > .env + echo "LLM_NAME=$llm_name" >> .env + echo "MODEL_NAME=$model_name" >> .env + echo "VITE_API_STREAMING=true" >> .env + echo -e "${GREEN}.env file configured for ${BOLD}${provider_name}${NC}${GREEN}.${NC}" + + check_and_start_docker + + echo -e "\n${NC}Starting Docker Compose...${NC}" + docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" build && docker compose -f "${COMPOSE_FILE}" up -d + docker_compose_status=$? + + echo "Docker Compose Exit Status: $docker_compose_status" # Debug output + + if [ "$docker_compose_status" -ne 0 ]; then + echo -e "\n${RED}${BOLD}Error starting Docker Compose. Please ensure Docker Compose is installed and in your PATH.${NC}" + echo -e "${RED}Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/${NC}" + exit 1 # Indicate failure and EXIT SCRIPT + fi + + echo -e "\n${GREEN}DocsGPT is now configured to use ${BOLD}${provider_name}${NC}${GREEN} on http://localhost:5173${NC}" + echo -e "${YELLOW}You can stop the application by running: docker compose -f \"${COMPOSE_FILE}\" down${NC}" +} + + +# Main script execution +animate_dino + +while true; do # Main menu loop + clear # Clear screen before showing main menu again + prompt_main_menu + + case $main_choice in + 1) # Use DocsGPT Public API Endpoint + use_docs_public_api_endpoint + ;; + 2) # Serve Local (with Ollama) + serve_local_ollama + ;; + 3) # Connect Local Inference Engine + connect_local_inference_engine + ;; + 4) # Connect Cloud API Provider + connect_cloud_api_provider + ;; + *) + echo -e "\n${RED}Invalid choice. Please choose 1-4.${NC}" ; sleep 1 ;; + esac +done + +echo -e "\n${GREEN}${BOLD}DocsGPT Setup Complete.${NC}" + +exit 0 \ No newline at end of file diff --git a/tests/llm/test_anthropic.py b/tests/llm/test_anthropic.py index 689013c0..50ddbe29 100644 --- a/tests/llm/test_anthropic.py +++ b/tests/llm/test_anthropic.py @@ -46,6 +46,7 @@ class TestAnthropicLLM(unittest.TestCase): {"content": "question"} ] mock_responses = [Mock(completion="response_1"), Mock(completion="response_2")] + mock_tools = Mock() with patch("application.cache.get_redis_instance") as mock_make_redis: mock_redis_instance = mock_make_redis.return_value @@ -53,7 +54,7 @@ class TestAnthropicLLM(unittest.TestCase): mock_redis_instance.set = Mock() with patch.object(self.llm.anthropic.completions, "create", return_value=iter(mock_responses)) as mock_create: - responses = list(self.llm.gen_stream("test_model", messages)) + responses = list(self.llm.gen_stream("test_model", messages, tools=mock_tools)) self.assertListEqual(responses, ["response_1", "response_2"]) prompt_expected = "### Context \n context \n ### Question \n question" diff --git a/tests/llm/test_sagemaker.py b/tests/llm/test_sagemaker.py index d659d498..2b893a9a 100644 --- a/tests/llm/test_sagemaker.py +++ b/tests/llm/test_sagemaker.py @@ -76,7 +76,7 @@ class TestSagemakerAPILLM(unittest.TestCase): with patch.object(self.sagemaker.runtime, 'invoke_endpoint_with_response_stream', return_value=self.response) as mock_invoke_endpoint: - output = list(self.sagemaker.gen_stream(None, self.messages)) + output = list(self.sagemaker.gen_stream(None, self.messages, tools=None)) mock_invoke_endpoint.assert_called_once_with( EndpointName=self.sagemaker.endpoint, ContentType='application/json', diff --git a/tests/test_cache.py b/tests/test_cache.py index 4270a181..af2b5e00 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -12,18 +12,21 @@ def test_make_gen_cache_key(): {'role': 'system', 'content': 'test_system_message'}, ] model = "test_docgpt" + tools = None # Manually calculate the expected hash - expected_combined = f"{model}_{json.dumps(messages, sort_keys=True)}" + messages_str = json.dumps(messages) + tools_str = json.dumps(tools) if tools else "" + expected_combined = f"{model}_{messages_str}_{tools_str}" expected_hash = get_hash(expected_combined) - cache_key = gen_cache_key(*messages, model=model) + cache_key = gen_cache_key(messages, model=model, tools=None) assert cache_key == expected_hash def test_gen_cache_key_invalid_message_format(): # Test when messages is not a list with unittest.TestCase.assertRaises(unittest.TestCase, ValueError) as context: - gen_cache_key("This is not a list", model="docgpt") + gen_cache_key("This is not a list", model="docgpt", tools=None) assert str(context.exception) == "All messages must be dictionaries." # Test for gen_cache decorator @@ -35,14 +38,14 @@ def test_gen_cache_hit(mock_make_redis): mock_redis_instance.get.return_value = b"cached_result" # Simulate a cache hit @gen_cache - def mock_function(self, model, messages): + def mock_function(self, model, messages, stream, tools): return "new_result" messages = [{'role': 'user', 'content': 'test_user_message'}] model = "test_docgpt" # Act - result = mock_function(None, model, messages) + result = mock_function(None, model, messages, stream=False, tools=None) # Assert assert result == "cached_result" # Should return cached result @@ -58,7 +61,7 @@ def test_gen_cache_miss(mock_make_redis): mock_redis_instance.get.return_value = None # Simulate a cache miss @gen_cache - def mock_function(self, model, messages): + def mock_function(self, model, messages, steam, tools): return "new_result" messages = [ @@ -67,7 +70,7 @@ def test_gen_cache_miss(mock_make_redis): ] model = "test_docgpt" # Act - result = mock_function(None, model, messages) + result = mock_function(None, model, messages, stream=False, tools=None) # Assert assert result == "new_result" @@ -83,14 +86,14 @@ def test_stream_cache_hit(mock_make_redis): mock_redis_instance.get.return_value = cached_chunk @stream_cache - def mock_function(self, model, messages, stream): + def mock_function(self, model, messages, stream, tools): yield "new_chunk" messages = [{'role': 'user', 'content': 'test_user_message'}] model = "test_docgpt" # Act - result = list(mock_function(None, model, messages, stream=True)) + result = list(mock_function(None, model, messages, stream=True, tools=None)) # Assert assert result == ["chunk1", "chunk2"] # Should return cached chunks @@ -106,7 +109,7 @@ def test_stream_cache_miss(mock_make_redis): mock_redis_instance.get.return_value = None # Simulate a cache miss @stream_cache - def mock_function(self, model, messages, stream): + def mock_function(self, model, messages, stream, tools): yield "new_chunk" messages = [ @@ -117,7 +120,7 @@ def test_stream_cache_miss(mock_make_redis): model = "test_docgpt" # Act - result = list(mock_function(None, model, messages, stream=True)) + result = list(mock_function(None, model, messages, stream=True, tools=None)) # Assert assert result == ["new_chunk"]