diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..edd99399449dcddae16aa7ddfab0d94479f4693b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +**/node_modules +**/venv +**/.venv +**/__pycache__ +**/chromadb_storage +**/chroma_data +**/chroma_db +.git +.gitignore +.env +**/.env +.vscode +.cursor +**/.idea +**/.DS_Store +**/Thumbs.db +docker-compose.yml +Dockerfile +.dockerignore +*.md \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..f6b1f326ca4ab7cf0c8798856f8fe0020ff82d58 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,36 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/propose_release.yml b/.github/workflows/propose_release.yml new file mode 100644 index 0000000000000000000000000000000000000000..83cfa04b81b0347f3076fd387f909588a1ad06b5 --- /dev/null +++ b/.github/workflows/propose_release.yml @@ -0,0 +1,29 @@ +name: Propose Stable Release +on: + workflow_dispatch: + inputs: + release_type: + type: choice + description: Release Type + options: + - patch + - minor + - major +jobs: + update_version: + uses: neongeckocom/.github/.github/workflows/propose_semver_release.yml@master + with: + branch: dev + release_type: ${{ inputs.release_type }} + update_changelog: True + version_file: "multi_llm_chatbot_backend/app/version.py" + pull_changes: + uses: neongeckocom/.github/.github/workflows/pull_master.yml@master + needs: update_version + with: + pr_reviewer: neonreviewers + pr_assignee: ${{ github.actor }} + pr_draft: false + pr_title: ${{ needs.update_version.outputs.version }} + pr_body: ${{ needs.update_version.outputs.changelog }} + destination: main diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml new file mode 100644 index 0000000000000000000000000000000000000000..5555b5ad1abccd8f252ebe0470a9fb9e3168683f --- /dev/null +++ b/.github/workflows/publish_release.yml @@ -0,0 +1,31 @@ +# On every push to `main` (which happens when a release PR produced by +# Propose Stable Release is merged), read the current version and create a +# matching GitHub release. Docker image publishing is intentionally left +# out for now and can be added separately. + +name: Publish GitHub Release +on: + push: + branches: + - main + +jobs: + tag_release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Get Version + run: | + VERSION=$(python multi_llm_chatbot_backend/app/version.py) + echo "VERSION=${VERSION}" >> $GITHUB_ENV + - uses: ncipollo/release-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ env.VERSION }} + generateReleaseNotes: true diff --git a/.github/workflows/publish_test_build.yml b/.github/workflows/publish_test_build.yml new file mode 100644 index 0000000000000000000000000000000000000000..7eab80b1196a1f87d30f676d95e5a8a3cddfb546 --- /dev/null +++ b/.github/workflows/publish_test_build.yml @@ -0,0 +1,24 @@ +# Bumps the alpha suffix on every push to `dev` (except when the only +# change is to the version file itself, to prevent a publish loop). A +# GitHub prerelease is cut for each alpha so artifacts are easy to find. +# This project is not distributed via PyPI, so `publish_pypi` is disabled. + +name: Publish Alpha Build +on: + push: + branches: + - dev + paths-ignore: + - 'multi_llm_chatbot_backend/app/version.py' + - 'CHANGELOG.md' + +jobs: + publish_alpha_release: + uses: neongeckocom/.github/.github/workflows/publish_alpha_release.yml@master + secrets: inherit + with: + version_file: "multi_llm_chatbot_backend/app/version.py" + setup_py: "multi_llm_chatbot_backend/app/version.py" + publish_prerelease: true + publish_pypi: false + update_changelog: true diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..4bc0793ebc0a71fb126a8fcf87f889c8ae13e200 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,37 @@ +name: Run Unit Tests +on: + pull_request: + workflow_dispatch: + +jobs: + unit_tests: + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12'] + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + working-directory: multi_llm_chatbot_backend + run: | + pip install wheel + pip install -r requirements.txt -r test_requirements.txt + + - name: Run unit tests + working-directory: multi_llm_chatbot_backend + run: | + pytest app/tests/unit/ --junitxml=test-results/results.xml + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-py${{ matrix.python-version }} + path: multi_llm_chatbot_backend/test-results/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..8f7973cffda85e3e9b22f29f18ffd05431b75fc0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# main +.env +gcp_build.txt + +# backend +multi_llm_chatbot_backend/.env +multi_llm_chatbot_backend/chroma_db/ +multi_llm_chatbot_backend/chromadb_storage/ +multi_llm_chatbot_backend/.dockerignore +multi_llm_chatbot_backend/.env.deploy +multi_llm_chatbot_backend/Dockerfile + + + +# frontend +phd-advisor-frontend/.env.production +phd-advisor-frontend/.firebase/ +phd-advisor-frontend/.firebaserc +phd-advisor-frontend/firebase.json + +# Ignore all __pycache__ folders +**/__pycache__/ + + +# Python virtual environments +**/venv/ +.venv/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..fe76617140ca6e6c64281ed9ad6b4ae0e4416f26 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,74 @@ +# Changelog + +## [2.0.1a3](https://github.com/NeonGeckoCom/CCAI-Demo/tree/2.0.1a3) (2026-05-21) + +[Full Changelog](https://github.com/NeonGeckoCom/CCAI-Demo/compare/2.0.1a2...2.0.1a3) + +**Merged pull requests:** + +- Add Onboarding tour [\#48](https://github.com/NeonGeckoCom/CCAI-Demo/pull/48) ([NeonRyan](https://github.com/NeonRyan)) + +## [2.0.1a2](https://github.com/NeonGeckoCom/CCAI-Demo/tree/2.0.1a2) (2026-05-19) + +[Full Changelog](https://github.com/NeonGeckoCom/CCAI-Demo/compare/33862e61789b40734f952d1ae06b03c86dff49af...2.0.1a2) + +**Implemented enhancements:** + +- \[FEAT\] Clear User Data [\#52](https://github.com/NeonGeckoCom/CCAI-Demo/issues/52) +- \[FEAT\] Support tools [\#34](https://github.com/NeonGeckoCom/CCAI-Demo/issues/34) +- \[FEAT\] Add endpoint and UI for User Account updates [\#26](https://github.com/NeonGeckoCom/CCAI-Demo/issues/26) +- \[FEAT\] Support advisor avatar images [\#25](https://github.com/NeonGeckoCom/CCAI-Demo/issues/25) +- \[FEAT\] Configurable User Guide [\#24](https://github.com/NeonGeckoCom/CCAI-Demo/issues/24) +- \[FEAT\] Support vLLM endpoints [\#22](https://github.com/NeonGeckoCom/CCAI-Demo/issues/22) +- \[FEAT\] Improved persona configuration handling [\#8](https://github.com/NeonGeckoCom/CCAI-Demo/issues/8) +- \[FEAT\] Implement automatic color picker for personas [\#7](https://github.com/NeonGeckoCom/CCAI-Demo/issues/7) +- \[FEAT\] Validate configured icons [\#6](https://github.com/NeonGeckoCom/CCAI-Demo/issues/6) +- \[FEAT\] Improved Checks for "Clarification Needed" [\#5](https://github.com/NeonGeckoCom/CCAI-Demo/issues/5) +- \[FEAT\] Versioning and release automation [\#4](https://github.com/NeonGeckoCom/CCAI-Demo/issues/4) + +**Fixed bugs:** + +- \[BUG\] Deleting a chat in the UI causes an empty chat to be created [\#45](https://github.com/NeonGeckoCom/CCAI-Demo/issues/45) +- \[BUG\] User inputs are sometimes repeated [\#42](https://github.com/NeonGeckoCom/CCAI-Demo/issues/42) +- \[BUG\] Improved method for LLM JSON Generation [\#13](https://github.com/NeonGeckoCom/CCAI-Demo/issues/13) + +**Closed issues:** + +- Pin the requirements.txt to stable versions [\#37](https://github.com/NeonGeckoCom/CCAI-Demo/issues/37) + +**Merged pull requests:** + +- feat/Added front end support for settings page [\#60](https://github.com/NeonGeckoCom/CCAI-Demo/pull/60) ([NeonRyan](https://github.com/NeonRyan)) +- Clear User Data [\#58](https://github.com/NeonGeckoCom/CCAI-Demo/pull/58) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Feat/improve clarification check [\#57](https://github.com/NeonGeckoCom/CCAI-Demo/pull/57) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- UI Improvements [\#56](https://github.com/NeonGeckoCom/CCAI-Demo/pull/56) ([NeonRyan](https://github.com/NeonRyan)) +- Fix bug around chat deletion [\#54](https://github.com/NeonGeckoCom/CCAI-Demo/pull/54) ([NeonRyan](https://github.com/NeonRyan)) +- Add User Guide [\#53](https://github.com/NeonGeckoCom/CCAI-Demo/pull/53) ([NeonRyan](https://github.com/NeonRyan)) +- Add versioning automation [\#47](https://github.com/NeonGeckoCom/CCAI-Demo/pull/47) ([NeonDaniel](https://github.com/NeonDaniel)) +- Fix/dup first message [\#44](https://github.com/NeonGeckoCom/CCAI-Demo/pull/44) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Feat/persona avatars [\#43](https://github.com/NeonGeckoCom/CCAI-Demo/pull/43) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Feat/account management [\#41](https://github.com/NeonGeckoCom/CCAI-Demo/pull/41) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Fix/json output [\#40](https://github.com/NeonGeckoCom/CCAI-Demo/pull/40) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Feat/advisor tools [\#38](https://github.com/NeonGeckoCom/CCAI-Demo/pull/38) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Feat/vllm endpoints [\#36](https://github.com/NeonGeckoCom/CCAI-Demo/pull/36) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Refactor/unittests [\#33](https://github.com/NeonGeckoCom/CCAI-Demo/pull/33) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Support Voice Interaction [\#32](https://github.com/NeonGeckoCom/CCAI-Demo/pull/32) ([NeonDaniel](https://github.com/NeonDaniel)) +- Fix/structured json output [\#30](https://github.com/NeonGeckoCom/CCAI-Demo/pull/30) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Feat/auto persona colors [\#23](https://github.com/NeonGeckoCom/CCAI-Demo/pull/23) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Feat/validate lucide icons [\#21](https://github.com/NeonGeckoCom/CCAI-Demo/pull/21) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Initial setup of GitHub Actions workflow [\#20](https://github.com/NeonGeckoCom/CCAI-Demo/pull/20) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Refactor chat UI layout [\#19](https://github.com/NeonGeckoCom/CCAI-Demo/pull/19) ([NeonDaniel](https://github.com/NeonDaniel)) +- Stream LLM Responses [\#18](https://github.com/NeonGeckoCom/CCAI-Demo/pull/18) ([NeonDaniel](https://github.com/NeonDaniel)) +- Feat/improve persona config [\#16](https://github.com/NeonGeckoCom/CCAI-Demo/pull/16) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Add Neon AI Branding [\#15](https://github.com/NeonGeckoCom/CCAI-Demo/pull/15) ([NeonDaniel](https://github.com/NeonDaniel)) +- Docs/update readme repo URL [\#14](https://github.com/NeonGeckoCom/CCAI-Demo/pull/14) ([NeonCharlie-24](https://github.com/NeonCharlie-24)) +- Improve backend logic around user input parsing [\#12](https://github.com/NeonGeckoCom/CCAI-Demo/pull/12) ([NeonDaniel](https://github.com/NeonDaniel)) +- Fix Home navigation to prevent logging out [\#11](https://github.com/NeonGeckoCom/CCAI-Demo/pull/11) ([NeonDaniel](https://github.com/NeonDaniel)) +- Add configuration for Undergrad Advisor Panel [\#10](https://github.com/NeonGeckoCom/CCAI-Demo/pull/10) ([NeonDaniel](https://github.com/NeonDaniel)) +- Add configuration support [\#3](https://github.com/NeonGeckoCom/CCAI-Demo/pull/3) ([NeonDaniel](https://github.com/NeonDaniel)) +- Docker optimization by Cursor [\#2](https://github.com/NeonGeckoCom/CCAI-Demo/pull/2) ([NeonDaniel](https://github.com/NeonDaniel)) +- Implement Docker deployment [\#1](https://github.com/NeonGeckoCom/CCAI-Demo/pull/1) ([NeonDaniel](https://github.com/NeonDaniel)) + + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e7131b1ac288c4693e1e85ef1191e465f588bb05 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,76 @@ +# syntax=docker/dockerfile:1.7 +# --------------------------------------------------------------------------- +# LaunchPadAI Panel โ€” single-container HuggingFace Spaces image. +# +# Mirrors the structural choices used by the working HF Spaces deployments +# CCAI-Vibe-Demo and CU-Student-AIProject-Helper: +# * multi-stage Node frontend build โ†’ Python+SPA bundle +# * non-root user uid 1000, port 7860 (HF Spaces convention) +# * persistence on the HF Storage Bucket mount at /data (SQLite shim) +# * REACT_APP_API_URL is set to the empty string at build time so the SPA +# issues relative URLs and shares the FastAPI origin +# +# Build context: repo root (this file). +# --------------------------------------------------------------------------- + +# ---- 1. Frontend build (CRA) ---------------------------------------------- +FROM node:20-bookworm AS frontend-build +WORKDIR /app/frontend + +COPY phd-advisor-frontend/package.json phd-advisor-frontend/package-lock.json* ./ +RUN --mount=type=cache,target=/root/.npm \ + npm ci + +COPY phd-advisor-frontend/ ./ + +# Empty REACT_APP_API_URL โ†’ CRA inlines '' so every fetch() hits the same +# origin as the SPA (the FastAPI server below). +ENV REACT_APP_API_URL="" +RUN npm run build + +# ---- 2. Python runtime + SPA bundle --------------------------------------- +FROM python:3.12-slim-bookworm + +# ffmpeg is required by the /api/voice/transcribe endpoint +# (browser WebM/Opus โ†’ WAV for Whisper). Kept on the runtime image only, +# not on the build image, so we don't pay the apt cost during incremental +# code rebuilds. +RUN apt-get update && \ + apt-get install -y --no-install-recommends ffmpeg && \ + rm -rf /var/lib/apt/lists/* + +# HF Spaces runs the container as uid 1000 and expects the app to do the +# same; with a non-root user we cannot bind privileged ports, but :7860 is +# fine. +RUN useradd -m -u 1000 user && mkdir -p /home/user/data && chown user:user /home/user/data +USER user +ENV HOME=/home/user \ + PATH=/home/user/.local/bin:$PATH \ + PYTHONUNBUFFERED=1 \ + CORS_ORIGINS=* \ + DATA_DIR=/home/user/data \ + CONFIG_PATH=/home/user/app/launchpad_config.yaml + +WORKDIR $HOME/app + +# ---- Python deps (cached unless requirements.txt changes) ----------------- +COPY --chown=user multi_llm_chatbot_backend/requirements.txt ./ +RUN --mount=type=cache,target=/home/user/.cache/pip,uid=1000,gid=1000 \ + pip install --no-cache-dir --user -r requirements.txt + +# ---- Backend source ------------------------------------------------------- +COPY --chown=user multi_llm_chatbot_backend/ ./ + +# ---- Top-level configuration files (config.yaml + persona definitions) ---- +COPY --chown=user launchpad_config.yaml ./launchpad_config.yaml +COPY --chown=user personas/ ./personas/ + +# ---- Frontend bundle ------------------------------------------------------ +# main.py mounts $HOME/app/static at "/" so the SPA is served same-origin +# with the API. +COPY --chown=user --from=frontend-build /app/frontend/build ./static + +ENV PYTHONPATH=$HOME/app + +EXPOSE 7860 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7bdfa3d8f506597d3fb28e47dd9b3ae5cb61bec5 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +--- +title: LaunchPadAI +emoji: ๐Ÿš€ +colorFrom: green +colorTo: blue +sdk: docker +pinned: false +app_port: 7860 +--- + +# LaunchPadAI Panel + +An AI undergraduate internship and early-career assistant built on Neon AI's +Collaborative Conversational AI (CCAI) framework. Ask about Handshake/LinkedIn +search, resume polish, interview prep, application cadence, and networking โ€” +and get diverse perspectives from a panel of five career AI advisors. + +This repository is a **complete, deployable application** โ€” the CCAI +multi-advisor stack (FastAPI backend + React frontend) wired to the +LaunchPadAI configuration in [`launchpad_config.yaml`](launchpad_config.yaml) +and the personas in [`personas/career_advisors/`](personas/career_advisors). + +## Advisors + +1. **Internship Search Strategist** โ€” roles, platforms, target lists +2. **Resume Optimizer** โ€” ATS, bullets, skills match +3. **Interview Coach** โ€” prep, presence, company research +4. **Application Scheduler** โ€” cadence, deadlines, tracking +5. **Career Path Mentor** โ€” networking, pivot, full-time transition + +## Hugging Face Spaces deployment + +This Space ships as a single Docker image built from the repository-root +[`Dockerfile`](Dockerfile). The container: + +1. Builds the React frontend (CRA) at image-build time with `REACT_APP_API_URL=""` + so every `fetch` issues a relative URL. +2. Serves the bundled SPA from FastAPI at `/`, with the API on `/api/...`, + `/auth/...`, etc. โ€” all on the same `:7860` origin. +3. Persists user data (auth, profiles, chat sessions) in **SQLite via + `aiosqlite`** at `${DATA_DIR}/launchpad_panel.db`. Mount a Hugging Face + Storage Bucket at `/data` to make the database survive Space rebuilds. + There is **no MongoDB** and no third-party data plane. + +### Required Space secrets + +| Secret | Purpose | +|--------|---------| +| `JWT_SECRET_KEY` | Signs auth tokens. Set this to a long random string. | +| `OPENAI_API_KEY` | Powers the default OpenAI provider (`gpt-5.4-mini`). | +| `GEMINI_API_KEY` | Optional โ€” only if you switch `llm.provider` back to `gemini`. | + +Set these under **Settings โ†’ Variables and secrets** on the Space. + +## Local deployment + +### Option A โ€” Docker + +```bash +# From the repo root, create a .env with at least: +# JWT_SECRET_KEY=some-long-random-string +# OPENAI_API_KEY=your-openai-key +docker compose up --build +``` + +Open . Override the host port with `LAUNCHPAD_HOST_PORT` if +7860 is taken. + +### Option B โ€” Native (no Docker) + +**Backend** (terminal 1): + +```bash +cd multi_llm_chatbot_backend +python -m venv venv && source venv/bin/activate +pip install -r requirements.txt +CONFIG_PATH=../launchpad_config.yaml uvicorn app.main:app --reload --port 8000 +``` + +**Frontend** (terminal 2): + +```bash +cd phd-advisor-frontend +npm install +REACT_APP_API_URL=http://localhost:8000 npm start +``` + +Open โ€” you should see **LaunchPadAI** with five career advisors. + +## Configuration + +App UI, login fields, chat examples, orchestrator keywords, and LLM/RAG +settings live in [`launchpad_config.yaml`](launchpad_config.yaml). The five +advisor YAMLs in `personas/career_advisors/` load automatically. Point the app at a +different config with `CONFIG_PATH`. + +The workshop draft that generated this panel is saved as +[`advisor-panel-draft.json`](advisor-panel-draft.json). diff --git a/advisor-panel-draft.json b/advisor-panel-draft.json new file mode 100644 index 0000000000000000000000000000000000000000..a0cc46d1441a39fdc446e452eaa80042a7c1d0e6 --- /dev/null +++ b/advisor-panel-draft.json @@ -0,0 +1,111 @@ +{ + "authorName": "Kennedy Johnson", + "panelName": "Undergraduate Internship Search", + "purpose": "Help undergraduate students locate, apply for, and land internships in a competitive market โ€” and pivot to entry-level full-time roles when appropriate.", + "advisors": [ + { + "name": "Internship Search Strategist", + "role": "Roles, Platforms & Target Lists" + }, + { + "name": "Resume Optimizer", + "role": "ATS, Bullets & Skills Match" + }, + { + "name": "Interview Coach", + "role": "Prep, Presence & Company Research" + }, + { + "name": "Application Scheduler", + "role": "Cadence, Deadlines & Tracking" + }, + { + "name": "Career Path Mentor", + "role": "Networking, Pivot & Full-Time Transition" + } + ], + "visionApplicationPurpose": "Help undergraduate students locate, apply for, and land internships in a competitive market โ€” and pivot to entry-level full-time roles when appropriate.", + "visionTargetAudience": "Undergraduate students, especially juniors (3rd year) seeking internships. Seniors (4th year+) are guided toward internships or full-time entry-level jobs.", + "visionKeyOutcomes": "Personalized daily/weekly application schedules; where to apply (Handshake, LinkedIn, company sites); interview tips and likely questions; resume feedback and revision based on experience and skills.", + "appTitle": "LaunchPadAI", + "appSubtitle": "AI mentor for internships to full-time jobs", + "primaryBrandColor": "#0F766E", + "logoIcon": "Rocket", + "footerText": "ยฉ 2026 Kennedy Johnson. All rights reserved.", + "headlinePrefix": "Undergraduate Internship Navigation", + "headlineHighlight": "AI Undergrad Job Finder", + "homepageDescription": "Receive a personalized plan to find and land an internship as a third-year undergrad, or pivot to an entry-level full-time job as a fourth-year student and beyond.", + "featuresSectionTitle": "AI Career Toolkit", + "featureCards": [ + { + "title": "AI Internship Search", + "icon": "Search", + "description": "A personalized plan to match multiple internships a day, plus resume review and interview tips to build your career foundation." + }, + { + "title": "Resume Optimizer", + "icon": "FilePenLine", + "description": "Feedback, edits, and tweaks so your resume stands out to ATS systems and recruiting teams." + }, + { + "title": "AI Interview Helper", + "icon": "MessagesSquare", + "description": "Near-perfect answers, company research tips, and guidance on presence and body language for virtual or in-person interviews." + } + ], + "loginPageSubtitle": "Sign in to land your internship and entry-level job today!", + "signupPageSubtitle": "Create an account to start the first day of your career!", + "userInfoRows": [ + { + "infoBoxLabel": "Academic Standing", + "dropdownChoices": "Sophomore (2nd year), Junior (3rd year โ€” internships), Senior (4th year โ€” FT / entry-level), Graduate / recent grad" + } + ], + "examplePromptCards": [ + { + "title": "Career Layout", + "icon": "CalendarRange", + "prompts": [ + "How many internships should I apply to each day?", + "How do I stand out in an interview?", + "Where can I find entry-level jobs that build real skills?" + ] + }, + { + "title": "Search & Apply", + "icon": "Search", + "prompts": [ + "Build a Handshake and LinkedIn search plan for business analytics internships.", + "What companies should a Leeds undergrad target this semester?", + "How do I track applications without getting overwhelmed?" + ] + }, + { + "title": "Resume & Materials", + "icon": "FilePenLine", + "prompts": [ + "Rewrite my resume bullets for an internship in marketing analytics.", + "What keywords should I add for ATS screening?", + "Review my experience section for a junior-year candidate." + ] + }, + { + "title": "Interview & Network", + "icon": "MessagesSquare", + "prompts": [ + "Give me 10 behavioral interview questions for a finance internship.", + "How should I research a company the night before?", + "Draft a LinkedIn outreach message to an alumni in my field." + ] + } + ], + "chatInputPlaceholder": "Ask about application cadence, Handshake/LinkedIn search, resume edits, or interview prep...", + "sharedBasePrompt": "base_prompt: |\n **Formatting (Compact Markdown v1):**\n - Use GitHub-Flavored Markdown.\n - Output exactly three sections in this order:\n - `### Thought` -- one sentence that reflects on the user's situation from YOUR perspective.\n - `### What to do` -- exactly 3 bullets, one line each, with practical actions.\n - `### Next step` -- one imperative sentence telling the user what to do now.\n - Use `###` for headings and `-` for bullets.\n - Insert one blank line between sections.\n - Keep each section unique and non-repetitive.", + "includedPersonaIds": [ + "internship_search_strategist", + "resume_optimizer", + "interview_coach", + "application_scheduler", + "career_path_mentor" + ] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..df5afbaf7b863caf38b7494b1e9cc5315ae12272 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +# --------------------------------------------------------------------------- +# Local-dev wrapper around the single-container HuggingFace Spaces image. +# +# Override LAUNCHPAD_HOST_PORT via .env (or shell) if 7860 conflicts; the +# in-container port is fixed at 7860 to mirror HF Spaces. +# --------------------------------------------------------------------------- + +services: + app: + build: + context: . + dockerfile: Dockerfile + image: launchpad-panel:dev + ports: + - "${LAUNCHPAD_HOST_PORT:-7860}:7860" + environment: + JWT_SECRET_KEY: ${JWT_SECRET_KEY:-CHANGEME-by-overriding-in-dot-env-file} + GEMINI_API_KEY: ${GEMINI_API_KEY:-} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + VLLM_API_KEY: ${VLLM_API_KEY:-} + VLLM_API_USERNAME: ${VLLM_API_USERNAME:-} + GEMINI_MODEL: ${GEMINI_MODEL:-gemini-2.5-flash} + CONFIG_PATH: ${CONFIG_PATH:-/home/user/app/launchpad_config.yaml} + # Same-origin in single-container; only relevant for cross-origin dev. + CORS_ORIGINS: ${CORS_ORIGINS:-*} + # SQLite + ChromaDB persistence both land here. The named volume below + # mirrors the HF Storage Bucket mount so user data survives rebuilds. + DATA_DIR: /data + volumes: + - launchpad_data:/data + +volumes: + launchpad_data: diff --git a/docs/nutrition_tools.md b/docs/nutrition_tools.md new file mode 100644 index 0000000000000000000000000000000000000000..e3517ec3e6281f69d0c4d6754f6322460fe0f6d9 --- /dev/null +++ b/docs/nutrition_tools.md @@ -0,0 +1,35 @@ +# Nutrition calculation tools (MuscleGrowthAI) + +Deterministic fitness/nutrition math for advisor responses. LLM personas receive pre-computed values instead of doing arithmetic themselves. + +## Tools in use + +| Source | Cost | Status | Purpose | +|--------|------|--------|---------| +| **Local Python formulas** | Free | **Active** | lbsโ†”kg, protein g/kg (1.6โ€“2.2), meal splits | +| **USDA FoodData Central** | Free (API key) | Planned | Food nutrient lookup by name/barcode | +| **Open Food Facts** | Free (no key) | Planned | Product/barcode nutrient data | + +## Core formulas (local) + +- **Weight:** `kg = lbs ร— 0.45359237` +- **Protein (hypertrophy):** `protein_g_min = round(weight_kg ร— 1.6)`, `protein_g_mid = round(weight_kg ร— 1.9)`, `protein_g_max = round(weight_kg ร— 2.2)` +- **Meal split:** even distribution with remainder assigned to first meals + +Example: **230 lb** โ†’ **104.33 kg** โ†’ **167โ€“230 g protein/day**; midpoint at **1.9 g/kg** โ‰ˆ **~198 g/day** (4 meals โ‰ˆ 50/50/49/49 g at the midpoint). + +## How calculations reach advisors + +1. **Context injection (primary):** When chat history mentions protein and a body weight is present, `compute_protein_advisory_context()` runs in `_build_enhanced_context_for_persona()` and adds a `PRE-COMPUTED NUTRITION VALUES` JSON block to each persona system prompt. +2. **LLM tool calling (optional):** `calculate_protein_target` is registered in `app/tools/nutrition_calculator.py` and enabled via `muscle_growth_config.yaml` โ†’ `tools.nutrition_calculator.enabled: true`. The orchestrator may call it for direct tool responses when appropriate. + +## Files + +- `multi_llm_chatbot_backend/app/tools/nutrition_calculator.py` โ€” formulas + tool definition +- `multi_llm_chatbot_backend/app/core/improved_orchestrator.py` โ€” injects computed context into persona prompts +- `muscle_growth_config.yaml` โ€” enables `nutrition_calculator` tool + +## USDA / Open Food Facts (later) + +- USDA: register at [https://fdc.nal.usda.gov/api-key-signup](https://fdc.nal.usda.gov/api-key-signup), set `USDA_API_KEY` env var. +- Open Food Facts: `https://world.openfoodfacts.org/api/v2/product/{barcode}` โ€” no key required. diff --git a/launchpad_config.yaml b/launchpad_config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6bbe389ad505fd1270591ba3c1c7b4d9763af500 --- /dev/null +++ b/launchpad_config.yaml @@ -0,0 +1,202 @@ +# ============================================================================ +# LaunchPadAI โ€” Application Configuration +# Generated from advisor-panel-draft (Undergraduate Internship Search) by Kennedy Johnson +# Modeled on MuscleGrowthAI / CCAI multi-advisor panel pattern +# ============================================================================ + +app: + title: "LaunchPadAI" + subtitle: "AI mentor for internships to full-time jobs" + primary_color: "#0F766E" + logo_icon: "Rocket" + footer_text: "ยฉ 2026 Kennedy Johnson. All rights reserved." + user_avatars: + - { id: "rocket-teal", icon: "Rocket", color: "#0F766E", bg: "#CCFBF1" } + - { id: "user-blue", icon: "User", color: "#2563EB", bg: "#EFF6FF" } + - { id: "briefcase-slate", icon: "Briefcase", color: "#334155", bg: "#F1F5F9" } + - { id: "file-amber", icon: "FilePenLine", color: "#D97706", bg: "#FFFBEB" } + - { id: "messages-indigo", icon: "MessagesSquare", color: "#4F46E5", bg: "#EEF2FF" } + - { id: "calendar-rose", icon: "CalendarRange", color: "#E11D48", bg: "#FFF1F2" } + - { id: "search-cyan", icon: "Search", color: "#0891B2", bg: "#ECFEFF" } + - { id: "grad-emerald", icon: "GraduationCap", color: "#059669", bg: "#ECFDF5" } + +homepage: + headline_prefix: "Undergraduate Internship Navigation" + headline_highlight: "AI Undergrad Job Finder" + description: >- + Receive a personalized plan to find and land an internship as a third-year + undergrad, or pivot to an entry-level full-time job as a fourth-year student + and beyond โ€” search strategy, resume polish, interview prep, and application cadence. + features_title: "AI Career Toolkit" + features: + - title: "AI Internship Search" + description: "A personalized plan to match multiple internships a day, plus resume review and interview tips to build your career foundation." + icon: "Search" + - title: "Resume Optimizer" + description: "Feedback, edits, and tweaks so your resume stands out to ATS systems and recruiting teams." + icon: "FilePenLine" + - title: "AI Interview Helper" + description: "Near-perfect answers, company research tips, and guidance on presence for virtual or in-person interviews." + icon: "MessagesSquare" + +login: + subtitle: "Sign in to land your internship and entry-level job today!" + signup_subtitle: "Create an account to start the first day of your career!" + knowledge_levels: + - { value: "", label: "Select your academic standing" } + - { value: "sophomore", label: "Sophomore (2nd year)" } + - { value: "junior", label: "Junior (3rd year โ€” internships)" } + - { value: "senior", label: "Senior (4th year โ€” FT / entry-level)" } + - { value: "grad", label: "Graduate / recent grad" } + timezones: + - { value: "", label: "Select timezone (optional)" } + - { value: "America/New_York", label: "Eastern (US)" } + - { value: "America/Chicago", label: "Central (US)" } + - { value: "America/Denver", label: "Mountain (US)" } + - { value: "America/Los_Angeles", label: "Pacific (US)" } + - { value: "America/Anchorage", label: "Alaska (US)" } + - { value: "Pacific/Honolulu", label: "Hawaii (US)" } + - { value: "UTC", label: "UTC" } + academic_stages: + - { value: "", label: "Select your academic standing" } + - { value: "sophomore", label: "Sophomore (2nd year)" } + - { value: "junior", label: "Junior (3rd year โ€” internships)" } + - { value: "senior", label: "Senior (4th year โ€” FT / entry-level)" } + - { value: "grad", label: "Graduate / recent grad" } + +chat_page: + placeholder: "Ask about application cadence, Handshake/LinkedIn search, resume edits, or interview prep..." + examples: + - title: "Career Layout" + icon: "CalendarRange" + color: "#0F766E" + bg_color: "#CCFBF1" + suggestions: + - "How many internships should I apply to each day?" + - "How do I stand out in an interview?" + - "Where can I find entry-level jobs that build real skills?" + - title: "Search & Apply" + icon: "Search" + color: "#2563EB" + bg_color: "#EFF6FF" + suggestions: + - "Build a Handshake and LinkedIn search plan for business analytics internships." + - "What companies should a Leeds undergrad target this semester?" + - "How do I track applications without getting overwhelmed?" + - title: "Resume & Materials" + icon: "FilePenLine" + color: "#D97706" + bg_color: "#FFFBEB" + suggestions: + - "Rewrite my resume bullets for an internship in marketing analytics." + - "What keywords should I add for ATS screening?" + - "Review my experience section for a junior-year candidate." + - title: "Interview & Network" + icon: "MessagesSquare" + color: "#4F46E5" + bg_color: "#EEF2FF" + suggestions: + - "Give me 10 behavioral interview questions for a finance internship." + - "How should I research a company the night before?" + - "Draft a LinkedIn outreach message to an alumni in my field." + +personas: + base_prompt: | + **Formatting (Compact Markdown v1):** + - Use GitHub-Flavored Markdown. + - Output exactly three sections in this order: + - `### Thought` โ€” one sentence that reflects on the user's situation from YOUR perspective. + - `### What to do` โ€” exactly 3 bullets, one line each, with practical actions. + - `### Next step` โ€” one imperative sentence telling the user what to do now. + - Use `###` for headings and `-` for bullets. + - Insert one blank line between sections. + - Keep each section unique and non-repetitive. + - Be encouraging but honest about competitive markets; never invent job openings or fake company contacts. + - Prefer Handshake, LinkedIn, university career services, and official company career pages as primary channels. + personas_dir: "personas/career_advisors" + +orchestrator: + min_words_without_keywords: 6 + conversation_history_token_threshold: 4000 + specific_keywords: + - "internship" + - "intern" + - "job" + - "career" + - "resume" + - "cv" + - "cover letter" + - "handshake" + - "linkedin" + - "interview" + - "behavioral" + - "recruiter" + - "ats" + - "apply" + - "application" + - "networking" + - "alumni" + - "offer" + - "full-time" + - "entry-level" + - "sophomore" + - "junior" + - "senior" + - "leeds" + - "undergrad" + - "deadline" + - "portfolio" + clarification_questions: + - "What is your academic standing (sophomore, junior, senior) and target start term?" + - "Which majors, industries, or role types are you targeting?" + - "Are you optimizing for internships, full-time entry-level, or both?" + - "How many hours per week can you realistically spend on applications?" + clarification_suggestions: + - "Ask for a weekly Handshake and LinkedIn search plan" + - "Upload your resume for ATS-focused edits" + - "Request behavioral interview practice for your target role" + - "Get a daily application cadence with tracking tips" + +auth: + algorithm: "HS256" + token_expiry_minutes: 43200 + +mongodb: + database_name: "launchpad_advisor" + +llm: + provider: "openai" + gemini: + model: "gemini-2.5-flash" + ollama: + model: "llama3.2:1b" + vllm: + api_url: "" + api_key: "" + api_username: "guest" + model_id: "" + neon_persona_orchestrator: "vanilla" + neon_persona_advisors: "vanilla" + openai: + # Key comes from the OPENAI_API_KEY env var / HF Space secret (never committed). + api_key: "" + model: "gpt-5.4-mini" + orchestrator_reasoning_effort: "low" + persona_reasoning_effort: "none" + resilient: + race_timeout_seconds: 60 + +rag: + embedding_model: "all-MiniLM-L6-v2" + chroma_collection: "launchpad_documents" + +tools: + current_datetime: + enabled: true + default_timezone: "America/Denver" + nutrition_calculator: + enabled: false + +voice: + stt_endpoint: "https://whisper.neonaiservices.com" + tts_endpoint: "https://coqui.neonaiservices.com" diff --git a/multi_llm_chatbot_backend/.env.example b/multi_llm_chatbot_backend/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..f7df7def244c219a85ef7f9e0645218a76fd3714 --- /dev/null +++ b/multi_llm_chatbot_backend/.env.example @@ -0,0 +1,24 @@ +# Copy this file to `.env` (same directory) for a native, non-Docker local run. +# The backend auto-loads it on startup. + +# Path to the app config, relative to this backend directory. +CONFIG_PATH=../launchpad_config.yaml + +# Where the SQLite database + uploaded docs live. Any writable folder works; +# on a native run this replaces the container's /data mount. +DATA_DIR=./local_data + +# Signs auth tokens โ€” set to a long random string. +JWT_SECRET_KEY=change-me-to-a-long-random-string + +# Default provider is OpenAI GPT-5.4-mini (see launchpad_config.yaml -> llm.provider). +# Set your OpenAI API key here (get one from Clary, or use your own). +OPENAI_API_KEY= + +# Optional alternatives: +# GEMINI_API_KEY= # only if you switch llm.provider back to "gemini" +VLLM_API_KEY= + +# Frontend origins allowed to call this backend during native dev +# (CRA dev server defaults to port 3000). +CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 diff --git a/multi_llm_chatbot_backend/README.md b/multi_llm_chatbot_backend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..79dc0b1229e5f4a9761d913e4d784f4760728b70 --- /dev/null +++ b/multi_llm_chatbot_backend/README.md @@ -0,0 +1,178 @@ +# Multi-LLM Chatbot Backend + +A modular, extensible FastAPI backend for building an AI-powered research advisor chatbot that supports: +- Multiple AI personas with configurable tone and behavior +- Dynamic switching between Gemini (cloud) and Ollama (local) LLMs +- Chat session persistence and context memory +- Document upload, chunking, and retrieval using RAG +- Rich export features (PDF, DOCX, TXT) +- User authentication and JWT-based access control + +--- + +## Backend Architecture + +```text +User Input + โ†“ +/chat-stream โ†’ Orchestrator + โ†“ โ†™ โ†˜ + SessionManager ContextManager RAGManager + โ†“ โ†“ โ†“ + MongoDB Token Trimming ChromaDB + โ†“ โ†“ โ†“ + Persisted Chat & Doc Context โ†’ LLM (Gemini/Ollama) +``` + +--- + +## Features + +- Persona-based multi-agent conversation (`Theorist`, `Pragmatist`, etc.) +- Provider switching (Gemini โ†” Ollama) +- Context-aware response routing + top-K advisor selection +- PDF, DOCX, and TXT file upload and semantic retrieval +- Developer tools: debug personas, test RAG, export sessions +- Secure authentication and session scoping + +--- + +## Setup Instructions + +### 1. Clone and Configure Environment + +```bash +git clone https://github.com/yourorg/multi-llm-chatbot-backend +cd multi-llm-chatbot-backend +cp .env.example .env # already provided +``` + +### 2. Python Environment Setup + +```bash +python -m venv venv +source venv/bin/activate # or venv\Scripts\activate on Windows + +pip install -r requirements.txt +``` + +### 3. Run the Server + +```bash +uvicorn app.main:app --reload +``` + +> Server will be available at: `http://localhost:8000` + +--- + +## FastAPI Routing & Modules + +| Folder | Description | +|--------|-------------| +| [`app/api`](./api_README.md) | REST API endpoints for chat, auth, RAG, exports | +| [`app/core`](./core_README.md) | Main orchestration, context windows, database logic | +| [`app/llm`](./llm_README.md) | Gemini + Ollama LLM wrappers | +| [`app/models`](./models_README.md) | Persona and user schemas | +| [`app/utils`](./utils_README.md) | File parsing, summaries, exports, vector helpers | + +--- + +## Key Files + +### `main.py` + +- Loads env vars, sets up FastAPI instance with CORS and routers +- Calls `connect_to_mongo()` on startup and `close_mongo_connection()` on shutdown +- Imports and registers all routers (`auth`, `chat_sessions`, etc.) + +### `.env` (Sample Vars) + +```ini +# MongoDB +MONGODB_CONNECTION_STRING=mongodb://localhost:27017 +MONGODB_DATABASE_NAME=neon_ai_backend + +# Gemini API Key and model +GEMINI_API_KEY=... # Replace with real key +GEMINI_MODEL=gemini-2.0-flash + +# Default provider +DEFAULT_PROVIDER=gemini +``` + +### `requirements.txt` + +Includes: +- **FastAPI**, **Uvicorn**: API framework and server +- **httpx**: Async LLM request handler +- **motor**, **pymongo**: MongoDB async access +- **chromadb**, **sentence-transformers**: Vector database + embeddings +- **PyPDF2**, **docx2txt**, **reportlab**: Document parsing and PDF generation +- **passlib**, **python-jose**: Auth and security + +--- + +## Persona Design & Context Handling + +- Personas defined in `app/models/default_personas.py` +- Rich system prompts, styles, and epistemologies +- Responses routed through `ImprovedChatOrchestrator` +- Context trimmed and weighted via `ContextManager` + +--- + +## Switching LLM Providers + +You can hot-swap models via API: + +```http +POST /switch-provider +{ "provider": "gemini" } | { "provider": "ollama" } +``` + +> Also supported: `/switch-model`, `/current-model`, `/current-provider` + +--- + +## Document Upload + RAG + +- Upload PDFs, DOCX, or TXT to sessions +- Text is extracted โ†’ chunked โ†’ embedded โ†’ stored in ChromaDB +- Queried during conversation by persona-aware `EnhancedRAGManager` + +--- + +## Export Options + +| Format | Export Endpoint | +|--------|------------------| +| PDF | `/export-chat?format=pdf` | +| DOCX | `/export-chat?format=docx` | +| TXT | `/export-chat?format=txt` | +| Summary | `/chat-summary?format=pdf` | + +--- + +## Developer & Debug Endpoints + +| Endpoint | Purpose | +|----------|---------| +| `/debug/personas` | See registered advisors and prompts | +| `/debug/ranked-personas` | View top-K advisors for context | +| `/debug/rag-status` | Run sample search to test document index | + +--- + +## Status & Roadmap + +- [x] Multi-LLM backend ready (Gemini + Ollama) +- [x] Document RAG + export system +- [x] Session-aware persona routing +- [x] JWT Auth + MongoDB user handling +- [ ] UI enhancements and persona memory +- [ ] Persona fine-tuning support (future) + +--- + +For questions, contributions, or deployment help โ€” feel free to reach out! \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/__init__.py b/multi_llm_chatbot_backend/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/multi_llm_chatbot_backend/app/api/README.md b/multi_llm_chatbot_backend/app/api/README.md new file mode 100644 index 0000000000000000000000000000000000000000..30cd8bd9dd1b5ac6aad83e7580045951868826fb --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/README.md @@ -0,0 +1,175 @@ +# `app/api` โ€“ REST API Layer for Multi-LLM Chatbot + +This module defines the complete FastAPI-based HTTP interface for all backend features, including chat, session management, RAG operations, provider switching, and document interaction. + +Each file in this directory defines route groups (`APIRouter`) to modularize functionality. + +--- + +## API Directory Layout + +| File | Purpose | +|------|---------| +| `auth.py` | Handles user authentication (login, signup, token validation) | +| `chat.py` | Core routes for LLM-backed chat, reply-to-advisor, and multi-turn flow | +| `chat_sessions.py` | Stores user conversations and provides access to saved history | +| `debug.py` | Developer tools: debug personas, RAG tests, ranking advisor responses | +| `documents.py` | Upload, parse, index, and query documents via RAG | +| `provider.py` | Switch between Gemini and Ollama providers | +| `root.py` | Root `/` endpoint for heartbeat and versioning | +| `sessions.py` | Tracks and resets session-specific in-memory context | +| `utils.py` | Helpers used by multiple routers (e.g. session ID management) | + +--- + +## `auth.py` โ€“ User Authentication API + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/signup` | `POST` | Register a new user | +| `/login` | `POST` | Authenticate user and return access token | +| `/me` | `GET` | Return current logged-in user | +| `/healthcheck` | `GET` | Ping endpoint to check login status | + +Uses JWT-based Bearer token auth via FastAPI dependencies. + +--- + +## `chat.py` โ€“ Chat Interaction + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/chat-stream` | `POST` | Stream advisor responses as newline-delimited JSON | +| `/reply-to-advisor` | `POST` | Ask a question to a specific advisor/persona | + +These routes handle: +- Message routing via `ImprovedChatOrchestrator` +- Persona-wise response generation +- Embedding document-aware context +- Returning consistent message structure + +--- + +## `chat_sessions.py` โ€“ Persistent Storage of Conversations + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/chat-sessions` | `GET` | List all saved chat sessions | +| `/chat-sessions/{id}` | `GET` | Retrieve specific chat session | +| `/chat-sessions/{id}` | `DELETE` | Soft-delete a chat session | +| `/chat-sessions/save` | `POST` | Save in-memory session to MongoDB | + +Saves message history, metadata, and uploaded files. + +--- + +## `debug.py` โ€“ Developer Tools + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/debug/personas` | `GET` | List current personas, prompts, keywords | +| `/debug/ranked-personas` | `GET` | Return top advisors for current session | +| `/debug/rag-status` | `GET` | Run sample RAG query + return health info | + +Provides insight into: +- Persona prompt preview +- RAG test queries and indexed documents +- Session size + truncation status + +--- + +## `documents.py` โ€“ Document Upload and RAG + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/upload-document` | `POST` | Upload and parse a document for semantic search | +| `/search-documents` | `POST` | RAG search using text query and persona context | +| `/document-stats` | `GET` | Overview of documents uploaded to session | +| `/uploaded-files` | `GET` | Return list of uploaded file names | +| `/document-insights/{filename}` | `GET` | Get detailed metadata for a document | +| `/export-chat` | `GET` | Export current or stored chat session (PDF, TXT, DOCX) | +| `/chat-summary` | `GET` | Export summary generated by LLM (multi-format) | + +Supports file parsing (`PDF`, `DOCX`, `TXT`), chunking, embedding, and export. + +--- + +## `provider.py` โ€“ LLM Provider Control + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/current-provider` | `GET` | Return currently active provider and model | +| `/switch-provider` | `POST` | Dynamically switch between `gemini` and `ollama` | +| `/current-model` | `GET` | Get currently loaded model name | +| `/switch-model` | `POST` | Alias for switching based on model name | + +Changes are propagated by: +- Creating new LLM client +- Re-registering all personas + +--- + +## `sessions.py` โ€“ In-Memory Session Management + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/context` | `GET` | Return current session context (messages, documents, stats) | +| `/reset-session` | `POST` | Reset in-memory session or specific chat context | +| `/session-stats` | `GET` | Return stats like message count, file size, timestamps | +| `/active-sessions` | `GET` | Return list of all active in-memory sessions | +| `/cleanup-sessions` | `POST` | Manually trigger expired session cleanup | + +Supports ephemeral sessions and reusable chat contexts (e.g. for documents). + +--- + +## `utils.py` โ€“ Route-Level Utilities + +Defines shared helper: + +- `get_or_create_session_for_request(request)` +- `get_or_create_session_for_request_async(request)` + +These parse session cookies or generate new session IDs, crucial for maintaining separation across: +- In-memory ephemeral sessions +- Document-linked long-term sessions + +--- + +## `root.py` โ€“ API Healthcheck + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/` | `GET` | Return version + feature list | + +Simple heartbeat endpoint used for readiness probes and sanity checks. + +--- + +## Auth Flow Integration + +Most routes use: + +```python +Depends(get_current_active_user) +``` + +This ensures only logged-in users can: +- Upload and retrieve files +- Export summaries +- Save or delete chat sessions + +JWT tokens are passed via the `Authorization: Bearer ...` header. + +--- + +## High-Level Flow + +```text +Frontend โ†’ /chat-stream โ†’ orchestrator โ†’ personas โ†’ RAG + LLM โ†’ response[] + โ†˜ /upload-document โ†’ extractor โ†’ RAG chunks โ†’ indexed + โ†˜ /context or /reset-session โ†’ session_manager + โ†˜ /export-chat or /chat-summary โ†’ utils + formatter +``` + +--- \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/api/old_routes.py b/multi_llm_chatbot_backend/app/api/old_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..001905b1e3fb00f37ef25bf60276943a7fa799ec --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/old_routes.py @@ -0,0 +1,1001 @@ +import os +from fastapi import APIRouter, Body, HTTPException, Header, UploadFile, File, Request +from fastapi import Query +from typing import Optional, List +import httpx +from app.llm.llm_client import LLMClient +from app.llm.improved_gemini_client import ImprovedGeminiClient +from app.llm.improved_ollama_client import ImprovedOllamaClient +from app.models.persona import Persona +from app.core.improved_orchestrator import ImprovedChatOrchestrator +from app.core.session_manager import get_session_manager +from app.core.rag_manager import get_rag_manager +from app.models.default_personas import get_default_personas +from app.utils.document_extractor import extract_text_from_file +from app.utils.file_limits import is_within_upload_limit +from pydantic import BaseModel + +from fastapi.responses import StreamingResponse +from fastapi import Query +from app.utils.file_export import export_chat_as_file + +from app.utils.chat_summary import generate_summary_from_messages, parse_summary_to_blocks +from app.utils.file_export import prepare_export_response, generate_pdf_file_from_blocks +from app.version import __version__ + +import hashlib +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter() + +# Provider management (same as before) +current_provider = "gemini" +available_providers = ["ollama", "gemini"] + +def create_llm_client(provider: str = None) -> LLMClient: + """Create LLM client based on provider""" + if provider is None: + provider = current_provider + + if provider == "gemini": + try: + return ImprovedGeminiClient(model_name=os.getenv("GEMINI_MODEL")) + except ValueError as e: + logger.warning(f"Gemini API key not found, falling back to Ollama: {e}") + return ImprovedOllamaClient(model_name="llama3.2:1b") + elif provider == "ollama": + return ImprovedOllamaClient(model_name="llama3.2:1b") + else: + raise ValueError(f"Unknown provider: {provider}") + +# Initialize with default provider +llm = create_llm_client() +chat_orchestrator = ImprovedChatOrchestrator() +session_manager = get_session_manager() + +# Initialize personas +DEFAULT_PERSONAS = get_default_personas(llm) +for persona in DEFAULT_PERSONAS: + chat_orchestrator.register_persona(persona) + +# Keep all the same data models as before +class UserInput(BaseModel): + user_input: str + +class PersonaInput(BaseModel): + id: str + name: str + system_prompt: str + +class ChatMessage(BaseModel): + user_input: str + session_id: Optional[str] = None + response_length: Optional[str] = "medium" + +class ReplyToAdvisor(BaseModel): + user_input: str + advisor_id: str + original_message_id: Optional[str] = None + +class ProviderSwitch(BaseModel): + provider: str + +# ============================================================== +# SESSION MANAGEMENT COMPATIBILITY LAYER +# ============================================================== + +def get_or_create_session_for_request(request: Request, + session_id_override: Optional[str] = None) -> str: + """ + Get or create session for request using multiple strategies: + 1. Use provided session_id if given + 2. Use X-Session-ID header if present + 3. Use client IP as fallback for backward compatibility + 4. Create new session if nothing available + + This allows the old stateless API to work with session management + """ + # Strategy 1: Explicit session ID (for new clients) + if session_id_override: + return session_id_override + + # Strategy 2: Check for session header (optional for frontend) + session_header = request.headers.get("X-Session-ID") + if session_header: + return session_header + + # Strategy 3: Use client IP for backward compatibility + # This gives each client IP their own persistent session + client_ip = request.client.host if request.client else "unknown" + ip_session_id = f"ip_{client_ip}" + + # Get or create session for this IP + session = session_manager.get_session(ip_session_id) + return session.session_id + + + +# Helper functions (same as before) +def _is_valid_response(response: str, persona_id: str) -> bool: + """Validate response quality""" + if len(response) < 2 or len(response) > 5000: + return False + + confusion_indicators = [ + f"Thank you, Dr. {persona_id.title()}", + "Assistant:", + f"Dr. {persona_id.title()}", + "Assistant:", + f"Dr. {persona_id.title()} Advisor:", + "excellent discussion, Assistant" + ] + + return not any(indicator in response for indicator in confusion_indicators) + +def _get_persona_fallback(persona_id: str) -> str: + """Get persona-specific fallback responses""" + fallbacks = { + "methodologist": "Focus on ensuring your methodology aligns with your research question. What specific method are you considering?", + "theorist": "Consider the theoretical framework underlying your approach. What assumptions guide your thinking?", + "pragmatist": "Let's break this down into actionable steps. What's the most important thing you need to decide today?" + } + return fallbacks.get(persona_id, "I'd be happy to help. Could you provide more details?") + +# Provider management endpoints (EXACTLY THE SAME) +@router.get("/current-provider") +async def get_current_provider(): + return { + "current_provider": current_provider, + "available_providers": available_providers, + "model_info": { + "name": llm.model_name if hasattr(llm, 'model_name') else "gemini-2.0-flash", + "provider": current_provider + } + } + +@router.post("/switch-provider") +async def switch_provider(provider_data: ProviderSwitch): + global current_provider, llm + + if provider_data.provider not in available_providers: + raise HTTPException( + status_code=400, + detail=f"Unknown provider: {provider_data.provider}. Available: {available_providers}" + ) + + try: + current_provider = provider_data.provider + new_llm = create_llm_client(current_provider) + llm = new_llm + + new_personas = get_default_personas(new_llm) + chat_orchestrator.personas.clear() + for persona in new_personas: + chat_orchestrator.register_persona(persona) + + return { + "message": f"Successfully switched to {current_provider}", + "current_provider": current_provider, + "model_info": { + "name": new_llm.model_name if hasattr(new_llm, 'model_name') else "gemini-2.0-flash", + "provider": current_provider + } + } + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to switch to {provider_data.provider}: {str(e)}" + ) + +# Main chat endpoint +@router.post("/chat-sequential") +async def chat_sequential_enhanced(message: ChatMessage, request: Request): + """ + Enhanced sequential chat with intelligent persona ordering. + Returns responses in the order determined by LLM-based relevance ranking. + """ + try: + # Get or create session + session_id = get_or_create_session_for_request(request, message.session_id) + + # Add user message to session first (needed for persona ranking) + session = session_manager.get_session(session_id) + session.append_message("user", message.user_input) + + # Get intelligently ordered personas based on context + top_personas = await chat_orchestrator.get_top_personas( + session_id=session_id, + k=3 # Get top 3 most relevant personas + ) + + logger.info(f"Intelligent persona order for session {session_id}: {top_personas}") + + # Generate responses from personas in the intelligent order + responses = [] + + for persona_id in top_personas: + try: + # Generate response from this persona + persona_result = await chat_orchestrator.chat_with_persona( + user_input=message.user_input, + persona_id=persona_id, + session_id=session_id, + response_length=message.response_length or "medium" + ) + + + if "persona_name" in persona_result and "response" in persona_result: + responses.append({ + "persona": persona_result["persona_name"], + "persona_id": persona_result["persona_id"], + "response": persona_result["response"] + }) + elif persona_result.get("type") == "single_persona_response" and "persona" in persona_result: + persona_data = persona_result["persona"] + responses.append({ + "persona": persona_data["persona_name"], + "persona_id": persona_data["persona_id"], + "response": persona_data["response"] + }) + else: + # Fallback response + responses.append({ + "persona": chat_orchestrator.personas[persona_id].name, + "persona_id": persona_id, + "response": "I'm having trouble processing your question right now. Please try again." + }) + + except Exception as e: + logger.error(f"Error generating response for persona {persona_id}: {str(e)}") + # Error fallback + responses.append({ + "persona": chat_orchestrator.personas[persona_id].name, + "persona_id": persona_id, + "response": "I encountered an error while processing your question. Please try again." + }) + + # response format + return { + "type": "sequential_responses", + "responses": responses + } + + except Exception as e: + logger.error(f"Error in enhanced sequential chat: {str(e)}") + return { + "type": "error", + "responses": [{ + "persona": "System", + "response": "I'm having trouble processing your request. Could you please try again?" + }] + } + +@router.post("/chat/{persona_id}") +async def chat_with_specific_advisor(persona_id: str, input: UserInput, request: Request): + """Chat with a specific advisor - SAME INTERFACE""" + try: + if persona_id not in chat_orchestrator.personas: + raise HTTPException(status_code=404, detail=f"Persona '{persona_id}' not found") + + # Get session using compatibility layer + session_id = get_or_create_session_for_request(request) + + # Use new orchestrator + result = await chat_orchestrator.chat_with_persona( + user_input=input.user_input, + persona_id=persona_id, + session_id=session_id + ) + + # FIX: Handle the actual response structure from orchestrator + if result.get("type") == "single_persona_response" and "persona" in result: + # New expected structure + persona_data = result["persona"] + return { + "persona": persona_data["persona_name"], + "persona_id": persona_data["persona_id"], + "response": persona_data["response"] + } + elif "persona_id" in result and "response" in result: + # Current actual structure from orchestrator + return { + "persona": result["persona_name"], + "persona_id": result["persona_id"], + "response": result["response"] + } + elif result.get("type") == "error" or "error" in result: + # Error handling + return { + "persona": "System", + "response": result.get("error", "I'm having trouble generating a response right now. Please try again.") + } + else: + # Fallback + return { + "persona": "System", + "response": "I'm having trouble generating a response right now. Please try again." + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error in chat_with_specific_advisor: {e}") + return { + "persona": "System", + "response": "I'm having trouble generating a response right now. Please try again." + } + +# Reply to advisor endpoint (SAME INTERFACE) +@router.post("/reply-to-advisor") +async def reply_to_advisor(reply: ReplyToAdvisor, request: Request): + """Reply to a specific advisor - SAME INTERFACE""" + try: + if reply.advisor_id not in chat_orchestrator.personas: + raise HTTPException(status_code=404, detail=f"Advisor '{reply.advisor_id}' not found") + + # Get session using compatibility layer + session_id = get_or_create_session_for_request(request) + + # Use new orchestrator + result = await chat_orchestrator.chat_with_persona( + user_input=reply.user_input, + persona_id=reply.advisor_id, + session_id=session_id + ) + + if result["type"] == "single_persona_response": + persona_data = result["persona"] + return { + "type": "advisor_reply", + "persona": persona_data["persona_name"], + "persona_id": persona_data["persona_id"], + "response": persona_data["response"], + "original_message_id": reply.original_message_id + } + else: + return { + "type": "error", + "persona": "System", + "response": result.get("message", "I'm having trouble generating a reply right now. Please try again.") + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error in reply_to_advisor: {e}") + return { + "type": "error", + "persona": "System", + "response": "I'm having trouble generating a reply right now. Please try again." + } + +@router.post("/upload-document") +async def upload_document(file: UploadFile = File(...), request: Request = None): + """ + Enhanced document upload with better metadata tracking and user feedback + """ + try: + # Get or create session + session_id = get_or_create_session_for_request(request) + session = session_manager.get_session(session_id) + + # Validate file + MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB + if file.size and file.size > MAX_FILE_SIZE: + raise HTTPException(status_code=413, detail="File size exceeds 10MB limit") + + + # Read and validate file content + file_bytes = await file.read() + content = extract_text_from_file(file_bytes, file.content_type) + if not content.strip(): + raise HTTPException(status_code=400, detail="Document is empty or unreadable.") + + # Get enhanced RAG manager + rag_manager = get_rag_manager() + + # Determine file type for metadata + file_type_map = { + "application/pdf": "pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx", + "text/plain": "txt" + } + file_type = file_type_map.get(file.content_type, "unknown") + + # Add document to enhanced vector database + rag_result = rag_manager.add_document( + content=content, + filename=file.filename, + session_id=session_id, + file_type=file_type + ) + + if not rag_result["success"]: + raise HTTPException( + status_code=500, + detail=f"Failed to process document: {rag_result.get('error', 'Unknown error')}" + ) + + # Update session tracking + session.uploaded_files.append(file.filename) + session.total_upload_size += len(file_bytes) + + # Add enhanced document reference to session messages + doc_metadata = rag_result.get("document_metadata", {}) + doc_title = doc_metadata.get("title", file.filename) + + session.append_message( + "system", + f"Document uploaded: '{doc_title}' ({file.filename}) - " + f"{rag_result['chunks_created']} sections processed, " + f"~{rag_result['total_tokens']} tokens analyzed. " + f"You can now ask questions about this document by referencing it by name." + ) + + return { + "message": f"Document '{file.filename}' uploaded and processed successfully.", + "filename": file.filename, + "document_title": doc_title, + "chunks_created": rag_result['chunks_created'], + "total_tokens": rag_result['total_tokens'], + "file_type": file_type, + "can_reference_by_name": True, + "suggestions": [ + f"Try asking: 'What methodology does my {file.filename} propose?'", + f"Or: 'What are the key findings in {doc_title}?'", + f"Or: 'Compare the approach in my document with current best practices'" + ] + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error processing document upload: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}") + + +@router.get("/export-chat") +async def export_chat(request: Request, format: str = Query(..., regex="^(txt|pdf|docx)$")): + """ + Export the current chat context in the requested format. + """ + try: + session_id = get_or_create_session_for_request(request) + session = session_manager.get_session(session_id) + + if not session.messages: + return {"error": "No messages in this session."} + + return prepare_export_response(session.messages, format) + + except Exception as e: + logger.error(f"Error exporting chat: {str(e)}") + return {"error": "Failed to export chat.", "detail": str(e)} + + +@router.get("/chat-summary") +async def chat_summary( + request: Request, + format: str = Query("text", regex="^(txt|pdf|docx)$") +): + """ + Generate and return a summary of the current session chat. + Can return as plain txt, PDF, or DOCX. + """ + try: + session_id = get_or_create_session_for_request(request) + session = session_manager.get_session(session_id) + + if not session.messages: + return {"error": "No messages in this session."} + + llm = next(iter(chat_orchestrator.personas.values())).llm + summary_text = await generate_summary_from_messages(session.messages, llm) + + if format == "txt": + return prepare_export_response(summary_text, "txt", filename_prefix="chat_summary") + + elif format == "docx": + return prepare_export_response(summary_text, "docx", filename_prefix="chat_summary") + + elif format == "pdf": + # Parse and render using block formatting + blocks = [{"type": "heading", "text": "Chat Summary"}] + parse_summary_to_blocks(summary_text) + + file_stream = generate_pdf_file_from_blocks(blocks) + return StreamingResponse( + file_stream, + media_type="application/pdf", + headers={"Content-Disposition": "attachment; filename=chat_summary.pdf"} + ) + + except Exception as e: + logger.error(f"Error in chat-summary endpoint: {str(e)}") + return {"error": "Summary generation failed", "detail": str(e)} + + + + +# Add new endpoint to get document statistics +@router.get("/document-stats") +async def get_document_stats(request: Request): + """Get statistics about uploaded documents in vector database""" + try: + session_id = get_or_create_session_for_request(request) + rag_manager = get_rag_manager() + + stats = rag_manager.get_document_stats(session_id) + return stats + + except Exception as e: + logger.error(f"Error getting document stats: {str(e)}") + return {"total_chunks": 0, "total_documents": 0, "documents": []} + +# Get uploaded files (SAME INTERFACE) +@router.get("/uploaded-files") +async def get_uploaded_filenames(request: Request): + """Get uploaded files - SAME INTERFACE""" + try: + session_id = get_or_create_session_for_request(request) + session = session_manager.get_session(session_id) + return {"files": session.uploaded_files} + except Exception as e: + logger.error(f"Error getting uploaded files: {str(e)}") + return {"files": []} + +# Context endpoint (SAME INTERFACE) +@router.get("/context") +async def get_context(request: Request): + """Get context - ENHANCED with RAG information""" + try: + session_id = get_or_create_session_for_request(request) + session = session_manager.get_session(session_id) + + # Get RAG statistics + rag_stats = session.get_rag_stats() + + return { + "messages": session.messages, + "rag_info": { + "total_documents": rag_stats.get("total_documents", 0), + "total_chunks": rag_stats.get("total_chunks", 0), + "documents": rag_stats.get("documents", []) + } + } + except Exception as e: + logger.error(f"Error getting context: {str(e)}") + return {"messages": [], "rag_info": {"total_documents": 0, "total_chunks": 0}} + +@router.post("/reset-session") +async def reset_session(request: Request): + """Reset session - ENHANCED with RAG cleanup""" + try: + session_id = get_or_create_session_for_request(request) + + # Use the enhanced reset that clears both conversation and vector DB + success = session_manager.reset_session_completely(session_id) + + if success: + return {"status": "reset", "message": "Session and all documents reset successfully"} + else: + return {"status": "error", "message": "Failed to reset session"} + except Exception as e: + logger.error(f"Error resetting session: {e}") + return {"status": "error", "message": "Failed to reset session"} + + +# Legacy model endpoints (SAME INTERFACE) +@router.post("/switch-model") +async def switch_model(model_name: str = Body(...)): + """Legacy model switching - SAME INTERFACE""" + if "gemini" in model_name.lower(): + return await switch_provider(ProviderSwitch(provider="gemini")) + else: + return await switch_provider(ProviderSwitch(provider="ollama")) + +@router.get("/current-model") +async def get_current_model(): + """Legacy model info - SAME INTERFACE""" + model_name = llm.model_name if hasattr(llm, 'model_name') else "gemini-2.0-flash" + return { + "model": model_name, + "provider": current_provider + } + +@router.post("/search-documents") +async def search_documents(request: Request, query: str = Body(..., embed=True), persona: str = Body("", embed=True)): + """ + Search uploaded documents using RAG + + This endpoint allows direct document search for debugging/testing + """ + try: + session_id = get_or_create_session_for_request(request) + rag_manager = get_rag_manager() + + # Get persona context for search enhancement + persona_contexts = { + "methodologist": "methodology research design analysis", + "theorist": "theory theoretical framework conceptual", + "pragmatist": "practical application implementation" + } + persona_context = persona_contexts.get(persona, "") + + # Search documents + results = rag_manager.search_documents( + query=query, + session_id=session_id, + persona_context=persona_context, + n_results=5 + ) + + return { + "query": query, + "persona_filter": persona, + "results_count": len(results), + "results": results + } + + except Exception as e: + logger.error(f"Error searching documents: {str(e)}") + return {"query": query, "results_count": 0, "results": [], "error": str(e)} + +@router.get("/session-stats") +async def get_session_stats(request: Request): + """Get comprehensive session statistics including RAG data""" + try: + session_id = get_or_create_session_for_request(request) + stats = session_manager.get_session_stats(session_id) + return stats + except Exception as e: + logger.error(f"Error getting session stats: {str(e)}") + return {"error": str(e)} + + +@router.get("/debug/personas") +async def debug_personas(request: Request): + """Debug personas - ENHANCED with RAG information""" + try: + session_id = get_or_create_session_for_request(request) + session = session_manager.get_session(session_id) + + # Get RAG statistics + rag_manager = get_rag_manager() + rag_stats = rag_manager.get_document_stats(session_id) + + return { + "personas": { + pid: { + "name": persona.name, + "prompt": persona.system_prompt[:100] + "...", + "retrieval_keywords": chat_orchestrator._get_persona_context_keywords(pid) + } for pid, persona in chat_orchestrator.personas.items() + }, + "session_info": { + "context_length": len(session.messages), + "uploaded_files": session.uploaded_files, + "rag_stats": rag_stats + }, + "current_provider": current_provider, + "rag_enabled": True + } + except Exception as e: + logger.error(f"Error in debug endpoint: {str(e)}") + return { + "personas": {}, + "session_info": {"context_length": 0}, + "current_provider": current_provider, + "rag_enabled": False, + "error": str(e) + } + +@router.get("/debug/ranked-personas") +async def get_ranked_personas(request: Request, k: int = Query(3, ge=1, le=10)): + """ + Debug endpoint: Get top-k ranked personas based on current session context. + Uses LLM to rank based on latest conversation messages. + """ + try: + session_id = get_or_create_session_for_request(request) + + # Call the ranking method + top_personas = await chat_orchestrator.get_top_personas(session_id=session_id, k=k) + + # Include some metadata for debug purposes + return { + "ranked_personas": top_personas, + "available_personas": list(chat_orchestrator.personas.keys()), + "session_id": session_id + } + except Exception as e: + logger.error(f"Error in /debug/ranked-personas: {e}") + return { + "ranked_personas": [], + "error": str(e) + } + + +@router.post("/chat/{persona_id}") +async def chat_with_specific_persona(persona_id: str, message: ChatMessage, request: Request): + """ + Chat with a specific persona - Enhanced with RAG debugging + + This endpoint helps debug RAG integration by testing individual personas + """ + try: + session_id = get_or_create_session_for_request(request, message.session_id) + + # Validate persona exists + if persona_id not in chat_orchestrator.personas: + available_personas = list(chat_orchestrator.personas.keys()) + raise HTTPException( + status_code=400, + detail=f"Persona '{persona_id}' not found. Available: {available_personas}" + ) + + # Use the enhanced orchestrator method + result = await chat_orchestrator.chat_with_persona( + user_input=message.user_input, + persona_id=persona_id, + session_id=session_id, + response_length=message.response_length or "medium" + ) + + # Fix: Handle the response structure properly + if result.get("type") == "single_persona_response" and "persona" in result: + persona_data = result["persona"] + + # Add debugging information + result["debug_info"] = { + "persona_id": persona_id, + "session_id": session_id, + "query_length": len(message.user_input), + "rag_manager_available": True, + "used_documents": persona_data.get("used_documents", False), + "chunks_used": persona_data.get("document_chunks_used", 0) + } + + return result + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error in individual persona chat: {str(e)}") + return { + "type": "error", + "message": f"Error chatting with {persona_id}: {str(e)}", + "persona_id": persona_id + } + +@router.get("/debug/enhanced-personas") +async def debug_enhanced_personas(request: Request): + """ + Enhanced debug endpoint with document context information + """ + try: + session_id = get_or_create_session_for_request(request) + session = session_manager.get_session(session_id) + + # Get enhanced RAG statistics + rag_manager = get_rag_manager() + rag_stats = rag_manager.get_document_stats(session_id) + + # Analyze document awareness capabilities + document_analysis = {} + if rag_stats.get("documents"): + for doc in rag_stats["documents"]: + document_analysis[doc["filename"]] = { + "chunks_available": doc["chunks"], + "estimated_tokens": doc["estimated_tokens"], + "sections_identified": doc["sections"], + "content_types_detected": { + "has_methodology": doc.get("has_methodology", False), + "has_theory": doc.get("has_theory", False), + "has_references": doc.get("has_references", False) + } + } + + return { + "personas": { + pid: { + "name": persona.name, + "expertise_area": persona.name.split(" - ")[1] if " - " in persona.name else "General", + "prompt_quality": "enhanced" if len(persona.system_prompt) > 500 else "basic", + "document_handling_enabled": "document awareness" in persona.system_prompt.lower(), + "retrieval_keywords": chat_orchestrator._get_enhanced_persona_context_keywords(pid)[:100] + "...", + "temperature": getattr(persona, 'temperature', 5) + } for pid, persona in chat_orchestrator.personas.items() + }, + "session_info": { + "context_length": len(session.messages), + "uploaded_files": session.uploaded_files, + "rag_stats": rag_stats, + "document_analysis": document_analysis + }, + "system_capabilities": { + "document_name_recognition": True, + "cross_document_analysis": True, + "persona_specialized_retrieval": True, + "enhanced_attribution": True, + "query_document_detection": True + }, + "current_provider": current_provider, + "rag_enabled": True, + "enhancement_level": "advanced" + } + except Exception as e: + logger.error(f"Error in enhanced debug endpoint: {str(e)}") + return { + "error": str(e), + "enhancement_level": "error", + "rag_enabled": False + } + +@router.get("/document-insights/{filename}") +async def get_document_insights(filename: str, request: Request): + """ + NEW ENDPOINT: Get insights about a specific uploaded document + """ + try: + session_id = get_or_create_session_for_request(request) + rag_manager = get_rag_manager() + + # Get document statistics + stats = rag_manager.get_document_stats(session_id) + + # Find the specific document + document_info = None + for doc in stats.get("documents", []): + if doc["filename"] == filename: + document_info = doc + break + + if not document_info: + raise HTTPException(status_code=404, detail=f"Document {filename} not found") + + # Get a sample of content from this document + results = rag_manager.collection.get( + where={"session_id": session_id, "filename": filename}, + limit=3, + include=["documents", "metadatas"] + ) + + sample_sections = [] + if results["documents"]: + for doc, metadata in zip(results["documents"], results["metadatas"]): + sample_sections.append({ + "section": metadata.get("document_section", "unknown"), + "content_preview": doc[:200] + "..." if len(doc) > 200 else doc, + "keywords": metadata.get("keywords", "") + }) + + return { + "filename": filename, + "document_title": document_info.get("title", filename), + "file_type": document_info.get("file_type", "unknown"), + "statistics": { + "total_chunks": document_info["chunks"], + "estimated_tokens": document_info["estimated_tokens"], + "sections_identified": document_info["sections"] + }, + "content_analysis": { + "has_methodology": document_info.get("has_methodology", False), + "has_theory": document_info.get("has_theory", False), + "has_references": document_info.get("has_references", False) + }, + "sample_sections": sample_sections, + "suggested_queries": [ + f"What methodology does my {filename} propose?", + f"What are the key theoretical concepts in {filename}?", + f"What are the main findings in my {document_info.get('title', filename)}?", + f"How can I improve the approach described in {filename}?" + ] + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting document insights: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error analyzing document: {str(e)}") + +# Also add a debug endpoint to check RAG status: + +@router.get("/debug/rag-status") +async def debug_rag_status(request: Request): + """ + Debug endpoint to check RAG system status + """ + try: + session_id = get_or_create_session_for_request(request) + + # Get RAG manager + rag_manager = get_rag_manager() + + # Get session stats + session_stats = session_manager.get_session_stats(session_id) + + # Test a simple search + test_search = rag_manager.search_documents( + query="test methodology research", + session_id=session_id, + persona_context="", + n_results=3 + ) + + return { + "rag_manager_healthy": True, + "session_id": session_id, + "session_stats": session_stats.get("rag_stats", {}), + "test_search_results": len(test_search), + "test_search_details": [ + { + "relevance": chunk.get("relevance_score", 0), + "distance": chunk.get("distance", "unknown"), + "text_length": len(chunk.get("text", "")), + "filename": chunk.get("metadata", {}).get("filename", "unknown") + } + for chunk in test_search[:3] + ], + "persona_keywords": { + pid: chat_orchestrator._get_persona_context_keywords(pid) + for pid in chat_orchestrator.personas.keys() + } + } + + except Exception as e: + logger.error(f"Error in RAG debug: {str(e)}") + return { + "rag_manager_healthy": False, + "error": str(e), + "session_id": session_id if 'session_id' in locals() else "unknown" + } + +# Ask endpoint (SAME INTERFACE) +class PersonaQuery(BaseModel): + question: str + persona: str + +@router.post("/ask/") +async def ask_question(query: PersonaQuery, request: Request): + """Ask question - SAME INTERFACE""" + try: + session_id = get_or_create_session_for_request(request) + + # Use the new orchestrator + result = await chat_orchestrator.chat_with_persona( + user_input=query.question, + persona_id=query.persona, + session_id=session_id + ) + + if result["type"] == "single_persona_response": + response_text = result["persona"]["response"] + else: + response_text = result.get("message", "I'm having trouble responding right now.") + + return {"response": response_text} + + except Exception as e: + logger.error(f"Error in ask endpoint: {str(e)}") + return {"response": "I encountered an error. Please try again."} + + + +# Root endpoint (SAME INTERFACE) +@router.get("/") +def root(): + """Root endpoint - SAME INTERFACE with updated info""" + return { + "message": "Multi-LLM PhD Advisor Backend is up and running", + "version": __version__, + "features": [ + "Improved Session Management", + "Unified Context Handling", + "Ollama Support", + "Gemini API Support", + "Provider Switching" + ] + } \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/api/routes/__init__.py b/multi_llm_chatbot_backend/app/api/routes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..da847f72ac41c0144615005a9b7989b50afab66f --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/__init__.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter +from .chat import router as chat_router +from .documents import router as document_router +from .sessions import router as session_router +from .provider import router as provider_router +from .debug import router as debug_router +from .root import router as root_router +from .phd_canvas import router as canvas_router +from .voice import router as voice_router + +router = APIRouter() +router.include_router(chat_router) +router.include_router(document_router) +router.include_router(session_router) +router.include_router(provider_router) +router.include_router(debug_router) +router.include_router(root_router) +router.include_router(canvas_router) +router.include_router(voice_router) diff --git a/multi_llm_chatbot_backend/app/api/routes/auth.py b/multi_llm_chatbot_backend/app/api/routes/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..9ade46476df93b2a7a9d640c40452afae0ced043 --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/auth.py @@ -0,0 +1,319 @@ +from fastapi import APIRouter, HTTPException, Depends, status +from datetime import datetime, timedelta +from app.models.user import UserCreate, UserLogin, User, Token, UserResponse, UserUpdate +from pydantic import BaseModel, model_validator +from typing import Optional +from app.core.auth import ( + get_password_hash, + verify_password, + authenticate_user, + create_access_token, + get_user_by_email, + get_current_active_user, + create_user_response, + ACCESS_TOKEN_EXPIRE_MINUTES +) +from app.core.database import get_database +import logging + +logger = logging.getLogger(__name__) + + +class MessageResponse(BaseModel): + message: str + + +class ChangePasswordRequest(BaseModel): + current_password: str + new_password: str + + @model_validator(mode="after") + def passwords_must_differ(self): + if self.current_password == self.new_password: + raise ValueError("New password must be different from the current password") + return self + + +class UpdateProfileRequest(BaseModel): + first_name: Optional[str] = None + last_name: Optional[str] = None + + @model_validator(mode="after") + def at_least_one_field(self): + if self.first_name is not None: + self.first_name = self.first_name.strip() or None + if self.last_name is not None: + self.last_name = self.last_name.strip() or None + if self.first_name is None and self.last_name is None: + raise ValueError("At least one field must be provided") + return self + + +class DeleteAccountRequest(BaseModel): + password: str + + +router = APIRouter() + +@router.post("/signup", response_model=Token) +async def signup(user_data: UserCreate): + """ + Register a new user and return an access token. + @param user_data: UserCreate with name, email, password, and optional academic fields + @return: Token containing a JWT access token and the created UserResponse + """ + try: + db = get_database() + + # Check if user already exists + existing_user = await get_user_by_email(user_data.email) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already registered" + ) + + # Create new user + hashed_password = get_password_hash(user_data.password) + user = User( + firstName=user_data.firstName, + lastName=user_data.lastName, + email=user_data.email, + hashed_password=hashed_password, + academicStage=user_data.academicStage, + researchArea=user_data.researchArea, + created_at=datetime.utcnow(), + is_active=True + ) + + # Insert user into database + result = await db.users.insert_one(user.dict(by_alias=True)) + user.id = result.inserted_id + + profile_seed: dict = { + "user_id": user.id, + "updated_at": datetime.utcnow(), + } + if user_data.academicStage: + profile_seed["knowledge_level"] = user_data.academicStage + if user_data.researchArea: + profile_seed["timezone"] = user_data.researchArea + await db.user_profiles.update_one( + {"user_id": user.id}, + {"$set": profile_seed}, + upsert=True, + ) + + # Create access token + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": str(user.id)}, + expires_delta=access_token_expires + ) + + return Token( + access_token=access_token, + token_type="bearer", + user=create_user_response(user) + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error during signup: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Could not create user account" + ) + +@router.post("/login", response_model=Token) +async def login(user_credentials: UserLogin): + """ + Authenticate a user and return an access token. + @param user_credentials: UserLogin with email and password + @return: Token containing a JWT access token and the authenticated UserResponse + """ + try: + # Authenticate user + user = await authenticate_user(user_credentials.email, user_credentials.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Update last login time + db = get_database() + await db.users.update_one( + {"_id": user.id}, + {"$set": {"last_login": datetime.utcnow()}} + ) + user.last_login = datetime.utcnow() + + # Create access token + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": str(user.id)}, + expires_delta=access_token_expires + ) + + return Token( + access_token=access_token, + token_type="bearer", + user=create_user_response(user) + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error during login: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Login failed" + ) + +@router.get("/me", response_model=UserResponse) +async def get_current_user_profile(current_user: User = Depends(get_current_active_user)): + """ + Retrieve the profile of the currently authenticated user. + @param current_user: Authenticated user from dependency injection + @return: UserResponse with the user's profile information + """ + return create_user_response(current_user) + +@router.post("/logout", response_model=MessageResponse) +async def logout(): + """ + Log out the current user (client should discard the token). + @return: MessageResponse with a confirmation message + """ + return MessageResponse(message="Successfully logged out") + +@router.post("/verify-token", response_model=UserResponse) +async def verify_token(current_user: User = Depends(get_current_active_user)): + """ + Validate the caller's JWT and return their profile. + @param current_user: Authenticated user from dependency injection + @return: UserResponse with the user's profile information + """ + return create_user_response(current_user) + +@router.post("/me/password", response_model=MessageResponse) +async def change_password( + body: ChangePasswordRequest, + current_user: User = Depends(get_current_active_user), +): + """ + Change the authenticated user's password. + @param body: ChangePasswordRequest with the current and new passwords + @param current_user: Authenticated user from dependency injection + @return: MessageResponse with a confirmation message + """ + try: + if not verify_password(body.current_password, current_user.hashed_password): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Current password is incorrect", + ) + if len(body.new_password) < 8: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="New password must be at least 8 characters", + ) + db = get_database() + await db.users.update_one( + {"_id": current_user.id}, + {"$set": {"hashed_password": get_password_hash(body.new_password)}}, + ) + return MessageResponse(message="Password changed successfully") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error during password change: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Could not change password" + ) + +@router.patch("/me", response_model=UserResponse) +async def update_profile( + body: UserUpdate, + current_user: User = Depends(get_current_active_user), +): + """Update account fields (name, email, avatar, signup preferences).""" + try: + db = get_database() + updates = {k: v for k, v in body.dict().items() if v is not None} + if body.firstName is not None: + updates["firstName"] = body.firstName.strip() + if body.lastName is not None: + updates["lastName"] = body.lastName.strip() + if not updates: + return create_user_response(current_user) + + if "email" in updates and updates["email"] != current_user.email: + existing = await get_user_by_email(updates["email"]) + if existing: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already in use", + ) + + await db.users.update_one({"_id": current_user.id}, {"$set": updates}) + profile_sync: dict = {"updated_at": datetime.utcnow()} + if body.academicStage is not None: + profile_sync["knowledge_level"] = body.academicStage + if body.researchArea is not None: + profile_sync["timezone"] = body.researchArea + if len(profile_sync) > 1: + await db.user_profiles.update_one( + {"user_id": current_user.id}, + {"$set": profile_sync, "$setOnInsert": {"user_id": current_user.id}}, + upsert=True, + ) + updated_user = await db.users.find_one({"_id": current_user.id}) + return create_user_response(User(**updated_user)) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error during profile update: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Could not update profile" + ) + +@router.delete("/me", response_model=MessageResponse) +async def delete_account( + body: DeleteAccountRequest, + current_user: User = Depends(get_current_active_user), +): + """ + Permanently delete the authenticated user's account, all chat sessions, and all PhD Canvas data. + @param body: DeleteAccountRequest with the user's password for confirmation + @param current_user: Authenticated user from dependency injection + @return: MessageResponse with a confirmation message + """ + try: + if not verify_password(body.password, current_user.hashed_password): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Incorrect password", + ) + db = get_database() + uid = current_user.id + await db.chat_sessions.delete_many({"user_id": uid}) + await db.phd_canvases.delete_many({"user_id": uid}) + await db.users.delete_one({"_id": uid}) + return MessageResponse(message="Account deleted") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error during account deletion: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Could not delete account" + ) \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/api/routes/chat.py b/multi_llm_chatbot_backend/app/api/routes/chat.py new file mode 100644 index 0000000000000000000000000000000000000000..743cd2ccf9e1e8a41bdaa84a7be539d154d7cf19 --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/chat.py @@ -0,0 +1,504 @@ +import asyncio +import json +import logging +import traceback +from typing import Any, Dict, List, Literal, Optional + +from bson import ObjectId +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.responses import StreamingResponse +from pydantic import BaseModel, Field + +from app.api.routes.chat_sessions import persist_message +from app.api.utils import get_or_create_session_for_request_async, load_chat_session_into_context +from app.core.auth import get_current_active_user +from app.core.bootstrap import chat_orchestrator +from app.core.database import get_database +from app.core.session_manager import get_session_manager +from app.models.user import User +from app.api.routes.user_profile import PROFILE_FIELDS, enrich_profile_from_user + +logger = logging.getLogger(__name__) + +router = APIRouter() +session_manager = get_session_manager() + + +async def _attach_user_profile_context(session, user: User) -> None: + """Load Mongo profile + signup fields into session for persona prompts.""" + try: + db = get_database() + doc = await db.user_profiles.find_one({"user_id": user.id}) + profile = enrich_profile_from_user(doc, user) + parts = [] + for key in PROFILE_FIELDS: + val = profile.get(key) + if val: + if isinstance(val, list): + val = ", ".join(str(v) for v in val) + parts.append(f"{key}: {val}") + if parts: + session.user_profile_context = "USER SECURITY PROFILE: " + "; ".join(parts) + except Exception as prof_err: + logger.warning(f"Could not load user profile: {prof_err}") + +# Enhanced data models +class UserInput(BaseModel): + user_input: str + +class ChatMessage(BaseModel): + user_input: str + session_id: Optional[str] = None + chat_session_id: Optional[str] = None # MongoDB chat session ID + response_length: str = "medium" + active_advisors: Optional[List[str]] = None + +class ReplyToAdvisor(BaseModel): + user_input: str + advisor_id: str + original_message_id: str = None + chat_session_id: Optional[str] = None + +class PersonaQuery(BaseModel): + question: str + persona: str + +class SwitchChatRequest(BaseModel): + chat_session_id: str + +class NewChatRequest(BaseModel): + title: Optional[str] = "New Chat" + +ChatStreamEventType = Literal["error", "progress", "clarification", "advisor"] + + +class ChatStreamLine(BaseModel): + """One NDJSON line from ``/chat-stream``.""" + + type: ChatStreamEventType + data: Dict[str, Any] = Field(default_factory=dict) + + def to_ndjson(self) -> str: + return json.dumps(self.model_dump(mode="json"), ensure_ascii=False) + "\n" + + +@router.post("/chat-stream") +async def chat_stream( + message: ChatMessage, + request: Request, + current_user: User = Depends(get_current_active_user), +) -> StreamingResponse: + """ + Streaming chat endpoint (newline-delimited JSON). + @param message: ChatMessage containing user input and optional session/chat IDs + @param request: FastAPI Request object for session management + @param current_user: Authenticated user from dependency injection + @return: StreamingResponse that yields ChatStreamLine events as NDJSON + """ + + async def _event_generator(): + try: + # Load or create the in-memory session + if message.chat_session_id: + sid = f"chat_{message.chat_session_id}" + if sid not in session_manager.sessions: + sid = await get_or_create_session_for_request_async( + request, + chat_session_id=message.chat_session_id, + user_id=str(current_user.id), + ) + elif len(session_manager.get_session(sid).messages) == 0: + # Cold start / new worker: hydrate from MongoDB before routing + loaded = await load_chat_session_into_context( + message.chat_session_id, + str(current_user.id), + ) + if loaded: + sid = loaded + else: + sid = await get_or_create_session_for_request_async(request) + + session = session_manager.get_session(sid) + await _attach_user_profile_context(session, current_user) + + # Append user message to in-memory session and persist to MongoDB + session.append_message("user", message.user_input) + if message.chat_session_id: + await persist_message(message.chat_session_id, { + "id": str(ObjectId()), + "type": "user", + "content": message.user_input, + }) + + if await chat_orchestrator.needs_clarification_improved(session, message.user_input): + clar = await chat_orchestrator.generate_contextual_clarification(message.user_input) + yield ChatStreamLine( + type="clarification", + data={ + "message": clar["question"], + "suggestions": clar["suggestions"], + }, + ).to_ndjson() + yield ChatStreamLine( + type="progress", + data={"phase": "complete"}, + ).to_ndjson() + return + + # If an enabled tool can handle this query, return its response + # directly and skip persona generation. + tool_result = await chat_orchestrator.get_tool_response(message.user_input) + if tool_result.used_tool: + session.append_message("orchestrator", tool_result.text) + yield ChatStreamLine( + type="advisor", + data={ + "persona_id": "orchestrator", + "persona_name": "Orchestrator", + "content": tool_result.text, + "used_documents": False, + "document_chunks_used": 0, + }, + ).to_ndjson() + yield ChatStreamLine( + type="progress", + data={"phase": "complete"}, + ).to_ndjson() + return + + # Always relevance-rank to the top 3 advisors, scoped to the + # user's active-advisor selection (the header dropdown) when one is + # provided. The dropdown filters the candidate pool; the LLM + # ranking still picks the top 3 from that pool. + if message.active_advisors: + candidate_ids = [ + pid for pid in message.active_advisors + if pid in chat_orchestrator.personas + ] + else: + candidate_ids = list(chat_orchestrator.personas.keys()) + top_personas = await chat_orchestrator.get_top_personas( + session_id=sid, + k=3, + candidate_ids=candidate_ids, + ) + + # Tell the client which advisors will respond so it can show + # thinking indicators for just those, not the entire active pool. + yield ChatStreamLine( + type="progress", + data={ + "phase": "selected", + "selected_advisors": top_personas, + }, + ).to_ndjson() + + done_queue: asyncio.Queue = asyncio.Queue() + + async def _run(pid: str) -> None: + try: + persona = chat_orchestrator.get_persona(pid) + result = await chat_orchestrator.generate_single_persona_response( + session, persona, + message.response_length or "medium", + ) + session.append_message(pid, result["response"]) + await done_queue.put(result) + except Exception as e: + logger.exception(f"chat-stream _run failed for {pid}: {e}") + failed_persona = chat_orchestrator.get_persona(pid) + await done_queue.put({ + "persona_id": pid, + "persona_name": failed_persona.name if failed_persona else pid, + "response": f"I ran into a technical issue. Please try again. ({e!s})", + "used_documents": False, + "document_chunks_used": 0, + }) + + tasks = [asyncio.create_task(_run(pid)) for pid in top_personas] + + for _ in range(len(tasks)): + result = await done_queue.get() + line = ChatStreamLine( + type="advisor", + data={ + "persona_id": result["persona_id"], + "persona_name": result["persona_name"], + "content": result["response"], + "used_documents": result.get("used_documents", False), + "document_chunks_used": result.get("document_chunks_used", 0), + }, + ) + yield line.to_ndjson() + + await asyncio.gather(*tasks, return_exceptions=True) + + yield ChatStreamLine( + type="progress", + data={"phase": "complete"}, + ).to_ndjson() + + except Exception as exc: + logger.error(f"chat-stream error: {exc}") + logger.error(traceback.format_exc()) + yield ChatStreamLine( + type="error", + data={"detail": str(exc)}, + ).to_ndjson() + + return StreamingResponse( + _event_generator(), + media_type="application/x-ndjson", + headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + }, + ) + + +@router.post("/switch-chat") +async def switch_to_chat( + request: SwitchChatRequest, + req: Request, + current_user: User = Depends(get_current_active_user) +): + """ + Switch to an existing chat session and load its context - FIXED VERSION + Ensures documents are accessible after switching + """ + try: + logger.info(f"Switching to chat session: {request.chat_session_id}") + + # Load the chat session into memory context with consistent session ID + memory_session_id = await get_or_create_session_for_request_async( + req, + chat_session_id=request.chat_session_id, + user_id=str(current_user.id) + ) + + if not memory_session_id: + raise HTTPException(status_code=404, detail="Chat session not found") + + logger.info(f"Loaded chat into memory session: {memory_session_id}") + + # Get the loaded session + session = session_manager.get_session(memory_session_id) + + # Verify document access after loading + rag_stats = session.get_rag_stats() + logger.info(f"After switch - Session {memory_session_id} has {rag_stats.get('total_documents', 0)} documents") + + # Get the original MongoDB chat session to retrieve messages in proper format + db = get_database() + chat_session = await db.chat_sessions.find_one({ + "_id": ObjectId(request.chat_session_id), + "user_id": current_user.id, + "is_active": True + }) + + if not chat_session: + raise HTTPException(status_code=404, detail="Chat session not found in database") + + # Return the messages in the original frontend format from MongoDB + original_messages = chat_session.get("messages", []) + + logger.info(f"Switch successful - {len(original_messages)} messages, {rag_stats.get('total_documents', 0)} documents") + + return { + "status": "success", + "memory_session_id": memory_session_id, + "chat_session_id": request.chat_session_id, + "message_count": len(original_messages), + "context": { + "messages": original_messages, # Return original format messages + "rag_info": rag_stats + }, + # Include document access verification + "document_access": { + "total_documents": rag_stats.get('total_documents', 0), + "total_chunks": rag_stats.get('total_chunks', 0), + "documents": rag_stats.get('documents', []), + "uploaded_files": session.uploaded_files + }, + "debug_info": { + "memory_session_format": memory_session_id, + "documents_accessible": rag_stats.get('total_documents', 0) > 0, + "session_loaded": memory_session_id in session_manager.sessions + } + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error switching to chat {request.chat_session_id}: {e}") + import traceback + logger.error(f"Full traceback: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail="Failed to switch to chat") + +@router.post("/new-chat") +async def create_new_chat( + request: NewChatRequest, + req: Request, + current_user: User = Depends(get_current_active_user) +): + """ + Create a new chat with fresh context + """ + try: + # Create a completely new session (no chat_session_id means fresh context) + memory_session_id = await get_or_create_session_for_request_async(req) + + # Ensure the session is completely clean + session = session_manager.get_session(memory_session_id) + session.clear_all_data() # This clears both messages and documents + + return { + "status": "success", + "memory_session_id": memory_session_id, + "message": "New chat created with fresh context", + "context": { + "messages": [], + "rag_info": {"total_documents": 0, "total_chunks": 0} + } + } + + except Exception as e: + logger.error(f"Error creating new chat: {e}") + raise HTTPException(status_code=500, detail="Failed to create new chat") + +@router.post("/chat/{persona_id}") +async def chat_with_specific_advisor(persona_id: str, input: UserInput, request: Request): + """Chat with a specific advisor - UPDATED""" + try: + if persona_id not in chat_orchestrator.personas: + raise HTTPException(status_code=404, detail=f"Persona '{persona_id}' not found") + + # Use async session management + session_id = await get_or_create_session_for_request_async(request) + + result = await chat_orchestrator.chat_with_persona( + user_input=input.user_input, + persona_id=persona_id, + session_id=session_id + ) + + # Handle response structure + if result.get("type") == "single_persona_response" and "persona" in result: + persona_data = result["persona"] + return { + "persona": persona_data["persona_name"], + "persona_id": persona_data["persona_id"], + "response": persona_data["response"] + } + elif "persona_id" in result and "response" in result: + return { + "persona": result["persona_name"], + "persona_id": result["persona_id"], + "response": result["response"] + } + else: + return { + "persona": "System", + "response": "I'm having trouble generating a response right now. Please try again." + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error in chat_with_specific_advisor: {e}") + return { + "persona": "System", + "response": "I'm having trouble generating a response right now. Please try again." + } + +@router.post("/reply-to-advisor") +async def reply_to_advisor(reply: ReplyToAdvisor, request: Request): + """Reply to a specific advisor with proper context - UPDATED""" + try: + if reply.advisor_id not in chat_orchestrator.personas: + raise HTTPException(status_code=404, detail=f"Advisor '{reply.advisor_id}' not found") + + # Handle session management for existing chats + if reply.chat_session_id: + session_id = f"chat_{reply.chat_session_id}" + else: + session_id = await get_or_create_session_for_request_async(request) + + session = session_manager.get_session(session_id) + + # Find the original message being replied to for context + original_message = None + if reply.original_message_id: + for msg in session.messages: + if getattr(msg, 'id', None) == reply.original_message_id: + original_message = msg.content + break + + # Create context-aware input + contextual_input = reply.user_input + if original_message: + contextual_input = f"[Replying to your previous message: '{original_message[:100]}...'] {reply.user_input}" + + result = await chat_orchestrator.chat_with_persona( + user_input=contextual_input, + persona_id=reply.advisor_id, + session_id=session_id + ) + + # Handle response structure + if result.get("type") == "single_persona_response" and "persona" in result: + persona_data = result["persona"] + return { + "type": "advisor_reply", + "persona": persona_data["persona_name"], + "persona_id": persona_data["persona_id"], + "response": persona_data["response"], + "original_message_id": reply.original_message_id + } + elif "persona_id" in result and "response" in result: + return { + "type": "advisor_reply", + "persona": result["persona_name"], + "persona_id": result["persona_id"], + "response": result["response"], + "original_message_id": reply.original_message_id + } + else: + return { + "type": "error", + "persona": "System", + "response": "I'm having trouble generating a reply right now. Please try again." + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error in reply_to_advisor: {e}") + return { + "type": "error", + "persona": "System", + "response": "I'm having trouble generating a reply right now. Please try again." + } + +@router.post("/ask/") +async def ask_question(query: PersonaQuery, request: Request): + """Ask question - UPDATED""" + try: + session_id = await get_or_create_session_for_request_async(request) + + result = await chat_orchestrator.chat_with_persona( + user_input=query.question, + persona_id=query.persona, + session_id=session_id + ) + + if result["type"] == "single_persona_response": + response_text = result["persona"]["response"] + else: + response_text = result.get("message", "I'm having trouble responding right now.") + + return {"response": response_text} + + except Exception as e: + logger.error(f"Error in ask endpoint: {str(e)}") + return {"response": "I encountered an error. Please try again."} diff --git a/multi_llm_chatbot_backend/app/api/routes/chat_sessions.py b/multi_llm_chatbot_backend/app/api/routes/chat_sessions.py new file mode 100644 index 0000000000000000000000000000000000000000..dcf7d25550630f839b3601c3432402a32743375e --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/chat_sessions.py @@ -0,0 +1,293 @@ +from fastapi import APIRouter, HTTPException, Depends, status +from typing import List, Optional +from datetime import datetime +from bson import ObjectId +from app.models.user import User, ChatSession, ChatSessionResponse +from app.core.auth import get_current_active_user +from app.core.database import get_database +from pydantic import BaseModel +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter() + +class CreateChatSessionRequest(BaseModel): + title: str + +class UpdateChatSessionRequest(BaseModel): + title: Optional[str] = None + messages: Optional[List[dict]] = None + +class SaveMessageRequest(BaseModel): + session_id: str + message: dict + +async def persist_message(session_id: str, message: dict): + """Write a single message to a MongoDB chat session.""" + db = get_database() + msg = message.copy() + if "timestamp" not in msg: + msg["timestamp"] = datetime.utcnow().isoformat() + await db.chat_sessions.update_one( + {"_id": ObjectId(session_id)}, + { + "$push": {"messages": msg}, + "$set": {"updated_at": datetime.utcnow()} + } + ) + + +@router.post("/chat-sessions", response_model=dict) +async def create_chat_session( + request: CreateChatSessionRequest, + current_user: User = Depends(get_current_active_user) +): + """Create a new chat session for the user""" + try: + db = get_database() + + session = ChatSession( + user_id=current_user.id, + title=request.title, + messages=[], + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + + result = await db.chat_sessions.insert_one(session.dict(by_alias=True)) + session.id = result.inserted_id + + return { + "id": str(session.id), + "title": session.title, + "created_at": session.created_at, + "updated_at": session.updated_at, + "message_count": 0 + } + + except Exception as e: + logger.error(f"Error creating chat session: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Could not create chat session" + ) + + +@router.get("/chat-sessions", response_model=List[ChatSessionResponse]) +async def get_user_chat_sessions( + current_user: User = Depends(get_current_active_user), + limit: int = 50, + skip: int = 0 +): + """Get all chat sessions for the current user""" + try: + db = get_database() + + cursor = db.chat_sessions.find( + {"user_id": current_user.id, "is_active": True} + ).sort("updated_at", -1).skip(skip).limit(limit) + + sessions = [] + async for session_data in cursor: + sessions.append(ChatSessionResponse( + id=str(session_data["_id"]), + title=session_data["title"], + created_at=session_data["created_at"], + updated_at=session_data["updated_at"], + message_count=len(session_data.get("messages", [])) + )) + + return sessions + + except Exception as e: + logger.error(f"Error fetching chat sessions: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Could not fetch chat sessions" + ) + + + +@router.get("/chat-sessions/count") +async def get_chat_sessions_count( + current_user: User = Depends(get_current_active_user) +): + """Get count of user's chat sessions""" + try: + db = get_database() + user_object_id = ObjectId(str(current_user.id)) + + count = await db.chat_sessions.count_documents({ + "user_id": user_object_id, + "is_active": {"$ne": False}, + "deleted_at": {"$exists": False} + }) + + return {"count": count} + + except Exception as e: + logger.error(f"Error counting chat sessions for user {current_user.id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to count chat sessions" + ) + + + +@router.get("/chat-sessions/{session_id}") +async def get_chat_session( + session_id: str, + current_user: User = Depends(get_current_active_user) +): + """Get a specific chat session with all messages""" + try: + db = get_database() + + session_data = await db.chat_sessions.find_one({ + "_id": ObjectId(session_id), + "user_id": current_user.id, + "is_active": True + }) + + if not session_data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Chat session not found" + ) + + return { + "id": str(session_data["_id"]), + "title": session_data["title"], + "messages": session_data.get("messages", []), + "created_at": session_data["created_at"], + "updated_at": session_data["updated_at"] + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching chat session: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Could not fetch chat session" + ) + +@router.put("/chat-sessions/{session_id}") +async def update_chat_session( + session_id: str, + request: UpdateChatSessionRequest, + current_user: User = Depends(get_current_active_user) +): + """Update a chat session (title or messages)""" + try: + db = get_database() + + # Verify session belongs to user + session_data = await db.chat_sessions.find_one({ + "_id": ObjectId(session_id), + "user_id": current_user.id, + "is_active": True + }) + + if not session_data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Chat session not found" + ) + + update_data = {"updated_at": datetime.utcnow()} + + if request.title is not None: + update_data["title"] = request.title + + if request.messages is not None: + update_data["messages"] = request.messages + + await db.chat_sessions.update_one( + {"_id": ObjectId(session_id)}, + {"$set": update_data} + ) + + return {"message": "Chat session updated successfully"} + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error updating chat session: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Could not update chat session" + ) + +@router.post("/chat-sessions/{session_id}/messages") +async def save_message_to_session( + session_id: str, + request: SaveMessageRequest, + current_user: User = Depends(get_current_active_user) +): + """Add a message to a chat session""" + try: + db = get_database() + + # Verify session belongs to user + session_data = await db.chat_sessions.find_one({ + "_id": ObjectId(session_id), + "user_id": current_user.id, + "is_active": True + }) + + if not session_data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Chat session not found" + ) + + await persist_message(session_id, request.message) + + return {"message": "Message saved successfully"} + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error saving message: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Could not save message" + ) + + + +@router.delete("/chat-sessions/{session_id}") +async def delete_chat_session( + session_id: str, + current_user: User = Depends(get_current_active_user) +): + """Delete a chat session (soft delete)""" + try: + db = get_database() + + result = await db.chat_sessions.update_one( + { + "_id": ObjectId(session_id), + "user_id": current_user.id + }, + {"$set": {"is_active": False, "updated_at": datetime.utcnow()}} + ) + + if result.matched_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Chat session not found" + ) + + return {"message": "Chat session deleted successfully"} + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error deleting chat session: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Could not delete chat session" + ) \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/api/routes/debug.py b/multi_llm_chatbot_backend/app/api/routes/debug.py new file mode 100644 index 0000000000000000000000000000000000000000..844c321f3833b1d3e3cd3d4b07d4cc5108d263be --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/debug.py @@ -0,0 +1,102 @@ +from fastapi import APIRouter, Request, Query +from app.core.session_manager import get_session_manager +from app.core.rag_manager import get_rag_manager +from app.core.bootstrap import chat_orchestrator +import logging + +from app.api.old_routes import get_or_create_session_for_request + +logger = logging.getLogger(__name__) + +router = APIRouter() + +session_manager = get_session_manager() + +@router.get("/debug/personas") +async def debug_personas(request: Request): + try: + session_id = get_or_create_session_for_request(request) + session = session_manager.get_session(session_id) + rag_manager = get_rag_manager() + rag_stats = rag_manager.get_document_stats(session_id) + + return { + "personas": { + pid: { + "name": persona.name, + "prompt": persona.system_prompt[:100] + "...", + "retrieval_keywords": chat_orchestrator._get_persona_context_keywords(pid) + } for pid, persona in chat_orchestrator.personas.items() + }, + "session_info": { + "context_length": len(session.messages), + "uploaded_files": session.uploaded_files, + "rag_stats": rag_stats + } + } + except Exception as e: + logger.error(f"Error in debug endpoint: {str(e)}") + return { + "personas": {}, + "session_info": {"context_length": 0}, + "error": str(e) + } + +@router.get("/debug/ranked-personas") +async def get_ranked_personas(request: Request, k: int = Query(3, ge=1, le=10)): + try: + session_id = get_or_create_session_for_request(request) + top_personas = await chat_orchestrator.get_top_personas(session_id=session_id, k=k) + return { + "ranked_personas": top_personas, + "available_personas": list(chat_orchestrator.personas.keys()), + "session_id": session_id + } + except Exception as e: + logger.error(f"Error in /debug/ranked-personas: {e}") + return { + "ranked_personas": [], + "error": str(e) + } + +@router.get("/debug/rag-status") +async def debug_rag_status(request: Request): + try: + session_id = get_or_create_session_for_request(request) + rag_manager = get_rag_manager() + session_stats = session_manager.get_session_stats(session_id) + + test_search = rag_manager.search_documents( + query="test methodology research", + session_id=session_id, + persona_context="", + n_results=3 + ) + + return { + "rag_manager_healthy": True, + "session_id": session_id, + "session_stats": session_stats.get("rag_stats", {}), + "test_search_results": len(test_search), + "test_search_details": [ + { + "relevance": chunk.get("relevance_score", 0), + "distance": chunk.get("distance", "unknown"), + "text_length": len(chunk.get("text", "")), + "filename": chunk.get("metadata", {}).get("filename", "unknown") + } + for chunk in test_search[:3] + ], + "persona_keywords": { + pid: chat_orchestrator._get_persona_context_keywords(pid) + for pid in chat_orchestrator.personas.keys() + } + } + + except Exception as e: + logger.error(f"Error in RAG debug: {str(e)}") + return { + "rag_manager_healthy": False, + "error": str(e), + "session_id": session_id if 'session_id' in locals() else "unknown" + } diff --git a/multi_llm_chatbot_backend/app/api/routes/documents.py b/multi_llm_chatbot_backend/app/api/routes/documents.py new file mode 100644 index 0000000000000000000000000000000000000000..4756e2761996a5bafa7dbd1b4d32cc618f44ba26 --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/documents.py @@ -0,0 +1,530 @@ +from fastapi import APIRouter, Request, HTTPException, UploadFile, File, Body, Depends +from fastapi import Query +from app.utils.document_extractor import extract_text_from_file +from app.core.session_manager import get_session_manager +from app.core.rag_manager import get_rag_manager +from app.api.utils import get_or_create_session_for_request_async +from fastapi.responses import StreamingResponse +from app.utils.chat_summary import generate_summary_from_messages, parse_summary_to_blocks, format_summary_for_text_export +from app.utils.file_export import prepare_export_response, generate_pdf_file_from_blocks +from app.core.session_manager import get_session_manager +from app.core.bootstrap import chat_orchestrator +from app.core.auth import get_current_active_user +from app.core.database import get_database +from app.models.user import User +from bson import ObjectId +import logging +import re +from html import unescape + + +logger = logging.getLogger(__name__) + +router = APIRouter() + +session_manager = get_session_manager() +get_rag_manager = get_rag_manager + + +def sanitize_html_content(content): + """ + Clean up HTML content by removing or fixing malformed tags. + This prevents PDF export errors caused by invalid HTML structure. + """ + if not content: + return content + + try: + logger.debug(f"Sanitizing content (first 200 chars): {content[:200]}") + + # First, unescape HTML entities + content = unescape(content) + + # More aggressive approach: Strip ALL HTML tags first, then apply simple formatting + # This prevents malformed HTML from causing issues + + # Remove all HTML tags completely (most aggressive approach) + content = re.sub(r'<[^>]*>', '', content) + + # Clean up multiple spaces and normalize whitespace + content = re.sub(r'\s+', ' ', content) + content = content.strip() + + # Remove any remaining HTML entities that might have been missed + content = re.sub(r'&[a-zA-Z0-9#]+;', '', content) + + # Remove any remaining angle brackets that might cause issues + content = content.replace('<', '').replace('>', '') + + logger.debug(f"Sanitized content (first 200 chars): {content[:200]}") + return content + + except Exception as e: + logger.error(f"Error sanitizing HTML content: {str(e)}") + # Ultra-fallback: return only alphanumeric and basic punctuation + try: + import string + allowed_chars = string.ascii_letters + string.digits + string.punctuation + ' \n\r\t' + cleaned = ''.join(c for c in content if c in allowed_chars) + return re.sub(r'\s+', ' ', cleaned).strip() + except: + return "Content could not be sanitized for export" + +def convert_messages_for_export(messages): + """ + Convert stored message format to export-compatible format. + Stored format uses 'type', export functions expect 'role' and specific structure. + """ + converted_messages = [] + + for i, msg in enumerate(messages): + try: + # Get and sanitize content + raw_content = msg.get('content', '') + sanitized_content = sanitize_html_content(raw_content) + + # Debug logging for problematic content + if i < 5 or '<' in raw_content or '>' in raw_content: # Log first few messages and any with HTML + logger.debug(f"Message {i}: Original length: {len(raw_content)}, Sanitized length: {len(sanitized_content)}") + if raw_content != sanitized_content: + logger.debug(f"Content changed during sanitization for message {msg.get('id', 'unknown')}") + + # Create base converted message + converted_msg = { + 'id': msg.get('id', 'unknown'), + 'timestamp': msg.get('timestamp', ''), + 'content': sanitized_content, + } + + # Convert type to role and handle different message types + msg_type = msg.get('type', 'unknown') + + if msg_type == 'user': + converted_msg['role'] = 'user' + # Add reply context if present + if 'replyTo' in msg: + reply_to = msg['replyTo'] + converted_msg['content'] = f"[Reply to {reply_to.get('advisorName', 'advisor')}] {converted_msg['content']}" + + elif msg_type == 'advisor': + converted_msg['role'] = 'assistant' + # Include advisor information + advisor_name = msg.get('advisorName', msg.get('persona', 'Advisor')) + converted_msg['advisor_name'] = advisor_name + converted_msg['advisor_id'] = msg.get('advisorId', msg.get('persona_id', 'unknown')) + + # Mark special response types + if msg.get('isReply'): + converted_msg['content'] = f"[{advisor_name} replies] {converted_msg['content']}" + elif msg.get('isExpansion'): + converted_msg['content'] = f"[{advisor_name} expands] {converted_msg['content']}" + else: + converted_msg['content'] = f"[{advisor_name}] {converted_msg['content']}" + + elif msg_type == 'system': + converted_msg['role'] = 'system' + + elif msg_type == 'document_upload': + converted_msg['role'] = 'system' + converted_msg['content'] = f"๐Ÿ“„ {converted_msg['content']}" + + elif msg_type == 'error': + converted_msg['role'] = 'system' + converted_msg['content'] = f"โŒ Error: {converted_msg['content']}" + + else: + # Unknown type, treat as system message + converted_msg['role'] = 'system' + converted_msg['content'] = f"[{msg_type}] {converted_msg['content']}" + + converted_messages.append(converted_msg) + + except Exception as e: + logger.error(f"Error converting message {msg.get('id', 'unknown')}: {str(e)}") + # Add a fallback message to maintain conversation flow + converted_messages.append({ + 'id': msg.get('id', 'unknown'), + 'role': 'system', + 'content': f"[Message conversion error: {str(e)}]", + 'timestamp': msg.get('timestamp', '') + }) + + logger.info(f"Converted {len(messages)} messages for export") + return converted_messages + + +@router.post("/upload-document") +async def upload_document( + file: UploadFile = File(...), + request: Request = None, + chat_session_id: str = Query(None, description="Chat session ID if uploading to specific chat"), + current_user: User = Depends(get_current_active_user) # ADDED: Require authentication +): + try: + if chat_session_id: + # If uploading to a specific chat, use chat_{id} format + session_id = f"chat_{chat_session_id}" + logger.info(f"Uploading document to specific chat session: {session_id}") + else: + # For new/temporary chats, use regular session management + session_id = await get_or_create_session_for_request_async(request) + logger.info(f"Uploading document to new session: {session_id}") + + # Add debug logging to track session IDs + logger.info(f"Document upload - chat_session_id parameter: {chat_session_id}") + logger.info(f"Document upload - final session_id: {session_id}") + logger.info(f"Document upload - user_id: {current_user.id}") + + session = session_manager.get_session(session_id) + + MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB + if file.size and file.size > MAX_FILE_SIZE: + raise HTTPException(status_code=413, detail="File size exceeds 10MB limit") + + file_bytes = await file.read() + content = extract_text_from_file(file_bytes, file.content_type) + if not content.strip(): + raise HTTPException(status_code=400, detail="Document is empty or unreadable.") + + rag_manager = get_rag_manager() + file_type_map = { + "application/pdf": "pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx", + "text/plain": "txt" + } + file_type = file_type_map.get(file.content_type, "unknown") + + # Pass the consistent session_id to RAG manager + logger.info(f"Adding document {file.filename} to session {session_id}") + rag_result = rag_manager.add_document( + content=content, + filename=file.filename, + session_id=session_id, + file_type=file_type + ) + + if not rag_result["success"]: + raise HTTPException(status_code=500, detail=f"Failed to process document: {rag_result.get('error', 'Unknown error')}") + + session.uploaded_files.append(file.filename) + session.total_upload_size += len(file_bytes) + + doc_metadata = rag_result.get("document_metadata", {}) + doc_title = doc_metadata.get("title", file.filename) + + session.append_message( + "system", + f"Document uploaded: '{doc_title}' ({file.filename}) - {rag_result['chunks_created']} sections processed, ~{rag_result['total_tokens']} tokens analyzed. You can now ask questions about this document by referencing it by name." + ) + + # Return session info for frontend tracking + return { + "message": f"Document '{file.filename}' uploaded and processed successfully.", + "filename": file.filename, + "document_title": doc_title, + "chunks_created": rag_result['chunks_created'], + "total_tokens": rag_result['total_tokens'], + "file_type": file_type, + "can_reference_by_name": True, + "session_id": session_id, + "chat_session_id": chat_session_id, + "user_id": str(current_user.id) # ADDED: Include user ID for debugging + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error processing document upload: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}") + + +@router.post("/search-documents") +async def search_documents(request: Request, query: str = Body(..., embed=True), persona: str = Body("", embed=True)): + try: + session_id = await get_or_create_session_for_request_async(request) # FIXED: Added await + rag_manager = get_rag_manager() + + persona_contexts = { + "methodologist": "methodology research design analysis", + "theorist": "theory theoretical framework conceptual", + "pragmatist": "practical application implementation" + } + persona_context = persona_contexts.get(persona, "") + + results = rag_manager.search_documents( + query=query, + session_id=session_id, + persona_context=persona_context, + n_results=5 + ) + + return { + "query": query, + "persona_filter": persona, + "results_count": len(results), + "results": results + } + + except Exception as e: + logger.error(f"Error searching documents: {str(e)}") + return {"query": query, "results_count": 0, "results": [], "error": str(e)} + + +@router.get("/document-stats") +async def get_document_stats(request: Request): + try: + session_id = await get_or_create_session_for_request_async(request) # FIXED: Added await + rag_manager = get_rag_manager() + return rag_manager.get_document_stats(session_id) + except Exception as e: + logger.error(f"Error getting document stats: {str(e)}") + return {"total_chunks": 0, "total_documents": 0, "documents": []} + + +@router.get("/uploaded-files") +async def get_uploaded_filenames(request: Request): + try: + session_id = await get_or_create_session_for_request_async(request) # FIXED: Added await + session = session_manager.get_session(session_id) + return {"files": session.uploaded_files} + except Exception as e: + logger.error(f"Error getting uploaded files: {str(e)}") + return {"files": []} + + +@router.get("/document-insights/{filename}") +async def get_document_insights(filename: str, request: Request): + try: + session_id = await get_or_create_session_for_request_async(request) # FIXED: Added await + rag_manager = get_rag_manager() + stats = rag_manager.get_document_stats(session_id) + document_info = next((doc for doc in stats.get("documents", []) if doc["filename"] == filename), None) + + if not document_info: + raise HTTPException(status_code=404, detail=f"Document {filename} not found") + + results = rag_manager.collection.get( + where={"session_id": session_id, "filename": filename}, + limit=3, + include=["documents", "metadatas"] + ) + + sample_sections = [] + if results["documents"]: + for doc, metadata in zip(results["documents"], results["metadatas"]): + sample_sections.append({ + "section": metadata.get("document_section", "unknown"), + "content_preview": doc[:200] + "..." if len(doc) > 200 else doc, + "keywords": metadata.get("keywords", "") + }) + + return { + "filename": filename, + "document_title": document_info.get("title", filename), + "file_type": document_info.get("file_type", "unknown"), + "statistics": { + "total_chunks": document_info["chunks"], + "estimated_tokens": document_info["estimated_tokens"], + "sections_identified": document_info["sections"] + }, + "content_analysis": { + "has_methodology": document_info.get("has_methodology", False), + "has_theory": document_info.get("has_theory", False), + "has_references": document_info.get("has_references", False) + }, + "sample_sections": sample_sections + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting document insights: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error analyzing document: {str(e)}") + +@router.get("/export-chat") +async def export_chat( + request: Request, + format: str = Query(..., regex="^(txt|pdf|docx)$"), + chat_session_id: str = Query(None, description="Optional: specific chat session ID to export"), + current_user: User = Depends(get_current_active_user) +): + """ + Export chat messages. + If chat_session_id is provided, exports that specific stored chat session. + Otherwise, exports the current in-memory session. + """ + try: + messages = [] + + if chat_session_id: + # Export specific stored chat session + db = get_database() + session_data = await db.chat_sessions.find_one({ + "_id": ObjectId(chat_session_id), + "user_id": current_user.id, + "is_active": True + }) + + if not session_data: + raise HTTPException( + status_code=404, + detail="Chat session not found or you don't have permission to access it" + ) + + raw_messages = session_data.get("messages", []) + # Convert stored message format to export-compatible format + messages = convert_messages_for_export(raw_messages) + else: + # Export current in-memory session (existing behavior) + session_id = await get_or_create_session_for_request_async(request) # FIXED: Added await + session = session_manager.get_session(session_id) + # In-memory messages might already be in the right format, but convert to be safe + messages = convert_messages_for_export(session.messages) + + if not messages: + return {"error": "No messages in this session."} + + try: + return prepare_export_response(messages, format) + except Exception as export_error: + logger.error(f"Error in prepare_export_response: {str(export_error)}") + # Try with a simplified version of messages if the export fails + try: + # Create simplified messages with just basic text content + simplified_messages = [] + for msg in messages: + simplified_msg = { + 'id': msg.get('id', 'unknown'), + 'role': msg.get('role', 'system'), + 'content': str(msg.get('content', '')).replace('\n', ' ').strip(), + 'timestamp': msg.get('timestamp', '') + } + if 'advisor_name' in msg: + simplified_msg['advisor_name'] = msg['advisor_name'] + simplified_messages.append(simplified_msg) + + return prepare_export_response(simplified_messages, format) + except Exception as fallback_error: + logger.error(f"Fallback export also failed: {str(fallback_error)}") + raise HTTPException( + status_code=500, + detail=f"Export failed due to content formatting issues. Please try a different format or contact support." + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error exporting chat: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Failed to export chat: {str(e)}" + ) + +@router.get("/chat-summary") +async def chat_summary( + request: Request, + format: str = Query("text", regex="^(txt|pdf|docx)$"), + chat_session_id: str = Query(None, description="Optional: specific chat session ID to summarize"), + current_user: User = Depends(get_current_active_user) +): + """ + Generate and return a summary of chat messages. + If chat_session_id is provided, summarizes that specific stored chat session. + Otherwise, summarizes the current in-memory session. + Can return as plain txt, PDF, or DOCX. + """ + try: + messages = [] + + if chat_session_id: + # Summarize specific stored chat session + db = get_database() + session_data = await db.chat_sessions.find_one({ + "_id": ObjectId(chat_session_id), + "user_id": current_user.id, + "is_active": True + }) + + if not session_data: + raise HTTPException( + status_code=404, + detail="Chat session not found or you don't have permission to access it" + ) + + raw_messages = session_data.get("messages", []) + # Convert stored message format for summary generation + messages = convert_messages_for_export(raw_messages) + else: + # Summarize current in-memory session (existing behavior) + session_id = await get_or_create_session_for_request_async(request) # FIXED: Added await + session = session_manager.get_session(session_id) + # Convert in-memory messages + messages = convert_messages_for_export(session.messages) + + if not messages: + return {"error": "No messages in this session."} + + try: + llm = next(iter(chat_orchestrator.personas.values())).llm + summary_text = await generate_summary_from_messages(messages, llm) + + if format == "txt": + # Use improved formatting for text export + formatted_summary = format_summary_for_text_export(summary_text) + return prepare_export_response(formatted_summary, "txt", filename_prefix="chat_summary") + + elif format == "docx": + # Use improved formatting for DOCX export + formatted_summary = format_summary_for_text_export(summary_text) + return prepare_export_response(formatted_summary, "docx", filename_prefix="chat_summary") + + elif format == "pdf": + # Parse and render using block formatting + blocks = [{"type": "heading", "text": "Chat Summary"}] + parse_summary_to_blocks(summary_text) + + file_stream = generate_pdf_file_from_blocks(blocks) + return StreamingResponse( + file_stream, + media_type="application/pdf", + headers={"Content-Disposition": "attachment; filename=chat_summary.pdf"} + ) + except Exception as summary_error: + logger.error(f"Error generating summary: {str(summary_error)}") + # Try with simplified content + try: + # Create a basic text summary if AI summary fails + basic_summary = "Chat Summary\n\n" + for msg in messages: + if msg.get('role') == 'user': + basic_summary += f"User: {msg.get('content', '')[:200]}...\n\n" + elif msg.get('role') == 'assistant': + advisor_name = msg.get('advisor_name', 'Advisor') + basic_summary += f"{advisor_name}: {msg.get('content', '')[:200]}...\n\n" + + if format == "txt": + return prepare_export_response(basic_summary, "txt", filename_prefix="chat_summary") + elif format == "docx": + return prepare_export_response(basic_summary, "docx", filename_prefix="chat_summary") + elif format == "pdf": + blocks = [{"type": "heading", "text": "Chat Summary"}, {"type": "paragraph", "text": basic_summary}] + file_stream = generate_pdf_file_from_blocks(blocks) + return StreamingResponse( + file_stream, + media_type="application/pdf", + headers={"Content-Disposition": "attachment; filename=chat_summary.pdf"} + ) + except Exception as fallback_error: + logger.error(f"Fallback summary export also failed: {str(fallback_error)}") + raise HTTPException( + status_code=500, + detail=f"Summary generation failed due to content formatting issues. Please try a different format." + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error in chat-summary endpoint: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Summary generation failed: {str(e)}" + ) \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/api/routes/onboarding.py b/multi_llm_chatbot_backend/app/api/routes/onboarding.py new file mode 100644 index 0000000000000000000000000000000000000000..98da98b9f79474261e334cf563ce8a87996495bd --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/onboarding.py @@ -0,0 +1,152 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All Rights Reserved 2008-2025 +# Licensed under the BSD 3-Clause License +# https://opensource.org/licenses/BSD-3-Clause +# +# Copyright (c) 2008-2025, Neongecko.com Inc. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import logging +from datetime import datetime +from typing import Any, Dict, Optional + +from fastapi import APIRouter, Depends +from pydantic import BaseModel + +from app.api.routes.user_profile import _is_field_filled, enrich_profile_from_user +from app.core.auth import get_current_active_user +from app.core.bootstrap import create_llm_client +from app.core.database import get_database +from app.core.onboarding_agent import OnboardingAgent, PROFILE_FIELDS +from app.models.user import User + +LOG = logging.getLogger(__name__) + +router = APIRouter() + +ONBOARDING_COLLECTION = "onboarding_conversations" + + +class OnboardingMessage(BaseModel): + user_input: str + + +def _progress(profile: Dict[str, Any]) -> int: + filled = sum(1 for k, *_ in PROFILE_FIELDS if _is_field_filled(profile.get(k))) + return int(filled / len(PROFILE_FIELDS) * 100) + + +def _next_missing_question(profile: Dict[str, Any]) -> Optional[str]: + """Return the human-friendly question for the first unfilled field.""" + for key, question, _desc in PROFILE_FIELDS: + if not _is_field_filled(profile.get(key)): + return question + return None + + +@router.get("/onboarding/start") +async def onboarding_start( + current_user: User = Depends(get_current_active_user), +) -> Dict[str, Any]: + """Return conversation history (if any) and current progress. + + If the user has an in-progress conversation it is returned so the + frontend can restore the chat. Otherwise a fresh contextual welcome + message is generated based on which fields are still missing. + """ + db = get_database() + doc = await db.user_profiles.find_one({"user_id": current_user.id}) + profile = enrich_profile_from_user(doc, current_user) + progress = _progress(profile) + + if progress >= 100: + await db[ONBOARDING_COLLECTION].delete_many({"user_id": current_user.id}) + return { + "messages": [{"role": "agent", + "text": "Your profile is already complete! Feel free to update anything by chatting here."}], + "progress": 100, + "complete": True, + } + + conv = await db[ONBOARDING_COLLECTION].find_one({"user_id": current_user.id}) + + if conv and conv.get("messages"): + return { + "messages": conv["messages"], + "progress": progress, + "complete": False, + } + + next_q = _next_missing_question(profile) + if progress == 0: + greeting = ( + f"Hey {current_user.firstName}! I'd like to learn a bit about your security background so " + "your advisors can tailor depth and examples. " + f"Let's start โ€” {next_q.lower() if next_q else 'tell me about your role and goals!'}" + ) + else: + greeting = ( + f"Welcome back, {current_user.firstName}! You're {progress}% done. " + f"Let's pick up where we left off โ€” {next_q.lower() if next_q else 'what else can you tell me?'}" + ) + + messages = [{"role": "agent", "text": greeting}] + await db[ONBOARDING_COLLECTION].update_one( + {"user_id": current_user.id}, + {"$set": {"messages": messages, "updated_at": datetime.utcnow()}, + "$setOnInsert": {"user_id": current_user.id}}, + upsert=True, + ) + + return {"messages": messages, "progress": progress, "complete": False} + + +@router.post("/onboarding/chat") +async def onboarding_chat( + msg: OnboardingMessage, + current_user: User = Depends(get_current_active_user), +) -> Dict[str, Any]: + db = get_database() + doc = await db.user_profiles.find_one({"user_id": current_user.id}) + profile = enrich_profile_from_user(doc, current_user) + + agent = OnboardingAgent(create_llm_client()) + result = await agent.chat(msg.user_input, current_user.id, profile) + + user_msg = {"role": "user", "text": msg.user_input} + agent_msg = {"role": "agent", "text": result["reply"]} + + await db[ONBOARDING_COLLECTION].update_one( + {"user_id": current_user.id}, + {"$push": {"messages": {"$each": [user_msg, agent_msg]}}, + "$set": {"updated_at": datetime.utcnow()}, + "$setOnInsert": {"user_id": current_user.id}}, + upsert=True, + ) + + if result.get("complete"): + await db[ONBOARDING_COLLECTION].delete_many({"user_id": current_user.id}) + + return result diff --git a/multi_llm_chatbot_backend/app/api/routes/phd_canvas.py b/multi_llm_chatbot_backend/app/api/routes/phd_canvas.py new file mode 100644 index 0000000000000000000000000000000000000000..53ba3791479e6208775f40c4fcb90e39ae07b2bc --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/phd_canvas.py @@ -0,0 +1,316 @@ +from fastapi import APIRouter, HTTPException, Depends, status, BackgroundTasks +from typing import Dict, Optional +from datetime import datetime +import logging +from bson import ObjectId + +from app.models.user import User +from app.models.phd_canvas import PhdCanvas, CanvasResponse, UpdateCanvasRequest +from app.core.auth import get_current_active_user +from app.core.database import get_database + +from pydantic import BaseModel + +logger = logging.getLogger(__name__) +router = APIRouter() + +# Request/Response models +class CanvasStatsResponse(BaseModel): + total_insights: int + total_sections: int + last_updated: Optional[str] = None + last_chat_processed: Optional[str] = None + created_at: Optional[str] = None + auto_update: bool = True + sections_breakdown: Dict = {} + +class PrintCanvasResponse(BaseModel): + user_id: str + generated_at: str + total_insights: int + last_updated: Optional[str] = None + sections: list + metadata: Dict + +def get_canvas_manager(): + """Lazy import to avoid circular dependency""" + from app.core.canvas_manager import get_canvas_manager + return get_canvas_manager() + +@router.get("/phd-canvas", response_model=CanvasResponse) +async def get_phd_canvas( + current_user: User = Depends(get_current_active_user) +): + """Get the user's PhD Canvas, creating it if it doesn't exist""" + try: + canvas_manager = get_canvas_manager() + canvas = await canvas_manager.get_or_create_canvas(str(current_user.id)) + + # Convert to response model + return CanvasResponse( + id=str(canvas.id), + user_id=str(canvas.user_id), + sections=canvas.sections, + created_at=canvas.created_at, + last_updated=canvas.last_updated, + last_chat_processed=canvas.last_chat_processed, + total_insights=canvas.total_insights, + auto_update=canvas.auto_update, + print_optimized=canvas.print_optimized + ) + + except Exception as e: + logger.error(f"Error getting PhD canvas for user {current_user.id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to retrieve PhD canvas" + ) + +@router.post("/phd-canvas/update", response_model=CanvasResponse) +async def update_phd_canvas( + request: UpdateCanvasRequest, + background_tasks: BackgroundTasks, + current_user: User = Depends(get_current_active_user) +): + """Update the PhD Canvas with latest insights from chat sessions""" + try: + canvas_manager = get_canvas_manager() + + # For large updates, run in background + if request.force_full_update: + logger.info(f"Queuing background canvas update for user {current_user.id}") + background_tasks.add_task( + _background_canvas_update, + str(current_user.id), + request + ) + + # Return current canvas immediately + canvas = await canvas_manager.get_or_create_canvas(str(current_user.id)) + else: + # For incremental updates, process immediately + logger.info(f"Processing incremental canvas update for user {current_user.id}") + canvas = await canvas_manager.update_canvas(str(current_user.id), request) + + return CanvasResponse( + id=str(canvas.id), + user_id=str(canvas.user_id), + sections=canvas.sections, + created_at=canvas.created_at, + last_updated=canvas.last_updated, + last_chat_processed=canvas.last_chat_processed, + total_insights=canvas.total_insights, + auto_update=canvas.auto_update, + print_optimized=canvas.print_optimized + ) + + except Exception as e: + logger.error(f"Error updating PhD canvas for user {current_user.id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to update PhD canvas" + ) + +@router.get("/phd-canvas/stats", response_model=CanvasStatsResponse) +async def get_canvas_stats( + current_user: User = Depends(get_current_active_user) +): + """Get statistics about the user's PhD Canvas""" + try: + canvas_manager = get_canvas_manager() + stats = await canvas_manager.get_canvas_stats(str(current_user.id)) + + return CanvasStatsResponse( + total_insights=stats.get("total_insights", 0), + total_sections=stats.get("total_sections", 0), + last_updated=stats.get("last_updated").isoformat() if stats.get("last_updated") else None, + last_chat_processed=stats.get("last_chat_processed").isoformat() if stats.get("last_chat_processed") else None, + created_at=stats.get("created_at").isoformat() if stats.get("created_at") else None, + auto_update=stats.get("auto_update", True), + sections_breakdown=stats.get("sections_breakdown", {}) + ) + + except Exception as e: + logger.error(f"Error getting canvas stats for user {current_user.id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to retrieve canvas statistics" + ) + +@router.get("/phd-canvas/print", response_model=PrintCanvasResponse) +async def get_print_canvas( + current_user: User = Depends(get_current_active_user) +): + """Get PhD Canvas formatted for printing""" + try: + canvas_manager = get_canvas_manager() + print_data = await canvas_manager.export_canvas_for_printing(str(current_user.id)) + + return PrintCanvasResponse( + user_id=print_data["user_id"], + generated_at=print_data["generated_at"].isoformat(), + total_insights=print_data["total_insights"], + last_updated=print_data["last_updated"].isoformat() if print_data["last_updated"] else None, + sections=print_data["sections"], + metadata=print_data["metadata"] + ) + + except Exception as e: + logger.error(f"Error exporting print canvas for user {current_user.id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to export canvas for printing" + ) + +@router.post("/phd-canvas/auto-update") +async def trigger_auto_update( + background_tasks: BackgroundTasks, + current_user: User = Depends(get_current_active_user) +): + """Trigger automatic canvas update (typically called when user opens canvas page)""" + try: + canvas_manager = get_canvas_manager() + db = get_database() + + # Get current canvas to check if auto-update is needed + canvas = await canvas_manager.get_or_create_canvas(str(current_user.id)) + + if not canvas.auto_update: + return {"message": "Auto-update is disabled for this canvas"} + + # Check if this is a first-time canvas (never processed any chats) + is_first_time_canvas = ( + canvas.last_chat_processed is None and + canvas.total_insights == 0 + ) + + if is_first_time_canvas: + logger.info(f"First-time canvas detected for user {current_user.id}, triggering full update") + + # For first-time canvas, always do full update to process all historical chats + background_tasks.add_task( + _background_canvas_update, + str(current_user.id), + UpdateCanvasRequest(force_full_update=True) + ) + + return { + "message": "First-time canvas update initiated", + "status": "processing", + "type": "full_update" + } + + # For existing canvas, check if there are actually new chats + user_object_id = ObjectId(str(current_user.id)) + + # Count chats newer than last processed + query_filter = { + "user_id": user_object_id, + "is_active": {"$ne": False}, + "deleted_at": {"$exists": False} + } + + if canvas.last_chat_processed: + query_filter["$or"] = [ + {"created_at": {"$gt": canvas.last_chat_processed}}, + {"updated_at": {"$gt": canvas.last_chat_processed}} + ] + + new_chat_count = await db.chat_sessions.count_documents(query_filter) + + if new_chat_count > 0: + logger.info(f"Found {new_chat_count} new/updated chats for user {current_user.id}, triggering incremental update") + + # Only update if there are actually new chats + background_tasks.add_task( + _background_canvas_update, + str(current_user.id), + UpdateCanvasRequest(force_full_update=False) + ) + + return { + "message": f"Canvas update queued for {new_chat_count} new chats", + "status": "updating", + "type": "incremental_update", + "new_chats": new_chat_count + } + else: + logger.info(f"No new chats found for user {current_user.id}, skipping update") + return { + "message": "Canvas is up to date", + "status": "current", + "new_chats": 0 + } + + except Exception as e: + logger.error(f"Error triggering auto-update for user {current_user.id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to trigger canvas auto-update" + ) + +@router.delete("/phd-canvas") +async def delete_canvas( + current_user: User = Depends(get_current_active_user) +): + """Delete the user's PhD Canvas""" + try: + canvas_manager = get_canvas_manager() + deleted = await canvas_manager.delete_canvas(str(current_user.id)) + + if deleted: + return {"message": "PhD Canvas deleted successfully"} + else: + return {"message": "No canvas found to delete"} + + except Exception as e: + logger.error(f"Error deleting canvas for user {current_user.id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to delete PhD canvas" + ) + +@router.get("/phd-canvas/refresh") +async def refresh_canvas_data( + background_tasks: BackgroundTasks, + current_user: User = Depends(get_current_active_user) +): + """Force refresh canvas with full update from all chats""" + try: + logger.info(f"Force refreshing canvas for user {current_user.id}") + + # Queue full update in background + background_tasks.add_task( + _background_canvas_update, + str(current_user.id), + UpdateCanvasRequest(force_full_update=True) + ) + + return { + "message": "Full canvas refresh initiated", + "status": "processing", + "estimated_completion": "2-3 minutes" + } + + except Exception as e: + logger.error(f"Error refreshing canvas for user {current_user.id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to refresh PhD canvas" + ) + +# Background task functions +async def _background_canvas_update(user_id: str, request: UpdateCanvasRequest): + """Background task to update canvas without blocking the API response""" + try: + logger.info(f"Starting background canvas update for user {user_id}") + canvas_manager = get_canvas_manager() + + updated_canvas = await canvas_manager.update_canvas(user_id, request) + + logger.info(f"Background canvas update completed for user {user_id}. Total insights: {updated_canvas.total_insights}") + + except Exception as e: + logger.error(f"Error in background canvas update for user {user_id}: {e}") + import traceback + logger.error(f"Traceback: {traceback.format_exc()}") \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/api/routes/provider.py b/multi_llm_chatbot_backend/app/api/routes/provider.py new file mode 100644 index 0000000000000000000000000000000000000000..11f22de76068d309f0f041e4af831e740b39be6b --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/provider.py @@ -0,0 +1,111 @@ +from fastapi import APIRouter, Body, HTTPException +from app.config import get_settings +from app.llm.improved_gemini_client import ImprovedGeminiClient +from app.llm.improved_ollama_client import ImprovedOllamaClient +from app.llm.improved_vllm_client import ImprovedVllmClient +from app.models.default_personas import get_default_personas +from app.core.bootstrap import chat_orchestrator, llm, current_provider, available_providers +from pydantic import BaseModel +import os +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter() + +def create_llm_client(provider: str = None): + global current_provider + if provider is None: + provider = current_provider + + if provider == "gemini": + try: + return ImprovedGeminiClient(model_name=os.getenv("GEMINI_MODEL")) + except ValueError as e: + logger.warning(f"Gemini API key not found, falling back to Ollama: {e}") + return ImprovedOllamaClient(model_name="llama3.2:1b") + elif provider == "ollama": + return ImprovedOllamaClient(model_name="llama3.2:1b") + elif provider == "vllm": + settings = get_settings() + if not settings.llm.vllm.api_url: + raise ValueError("No vLLM endpoint configured. Set llm.vllm.api_url in your config.") + return ImprovedVllmClient( + api_url=settings.llm.vllm.api_url, + api_key=settings.llm.vllm.api_key, + ) + else: + raise ValueError(f"Unknown provider: {provider}") + +# NOTE: Personas and `llm` are already created and registered by +# app/core/bootstrap.py with the correct ResilientLLMClient wrapping +# (primary vLLM + OpenAI fallback). Re-registering them here at module +# import time replaces them with a raw vLLM client that lacks the +# fallback wrapper AND lacks the api_username/api_key wiring, breaking +# both auth and failover. Bootstrap already handles initial setup. + +class ProviderSwitch(BaseModel): + provider: str + +@router.get("/current-provider") +async def get_current_provider(): + return { + "current_provider": current_provider, + "available_providers": available_providers, + "model_info": { + "name": llm.model_name if hasattr(llm, 'model_name') else "gemini-2.0-flash", + "provider": current_provider + } + } + +@router.post("/switch-provider") +async def switch_provider(provider_data: ProviderSwitch): + global current_provider, llm + + if provider_data.provider not in available_providers: + raise HTTPException(status_code=400, detail=f"Unknown provider: {provider_data.provider}. Available: {available_providers}") + + try: + from app.core.bootstrap import ( + create_orchestrator_llm, + create_persona_llm, + ) + + current_provider = provider_data.provider + new_llm = create_orchestrator_llm() + llm = new_llm + + chat_orchestrator.llm_client = new_llm + + persona_llm = create_persona_llm() + new_personas = get_default_personas(persona_llm) + chat_orchestrator.personas.clear() + for persona in new_personas: + chat_orchestrator.register_persona(persona) + + return { + "message": f"Successfully switched to {current_provider}", + "current_provider": current_provider, + "model_info": { + "name": new_llm.model_name if hasattr(new_llm, 'model_name') else "gemini-2.0-flash", + "provider": current_provider + } + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to switch to {provider_data.provider}: {str(e)}") + +@router.post("/switch-model") +async def switch_model(model_name: str = Body(...)): + if "gemini" in model_name.lower(): + return await switch_provider(ProviderSwitch(provider="gemini")) + else: + return await switch_provider(ProviderSwitch(provider="ollama")) + +@router.get("/current-model") +async def get_current_model(): + model_name = llm.model_name if hasattr(llm, 'model_name') else "gemini-2.0-flash" + return { + "model": model_name, + "provider": current_provider + } diff --git a/multi_llm_chatbot_backend/app/api/routes/root.py b/multi_llm_chatbot_backend/app/api/routes/root.py new file mode 100644 index 0000000000000000000000000000000000000000..af59047aa072edbcd54e0c8316fe1784ed3dab51 --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/root.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter +from app.config import get_settings +from app.version import __version__ + +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter() + +# NB: keep this OFF the bare "/" path. In the single-container / Hugging Face +# Spaces deployment the React SPA is mounted at "/" as a catch-all, and any +# API route registered at exactly "/" would shadow the SPA's index.html and +# leave users staring at this JSON banner instead of the app. +@router.get("/api/health") +def root(): + title = get_settings().app.title + return { + "message": f"{title} Backend is up and running", + "version": __version__, + "features": [ + "Configurable Personas", + "Improved Session Management", + "Unified Context Handling", + "Ollama Support", + "Gemini API Support", + "Provider Switching" + ] + } + diff --git a/multi_llm_chatbot_backend/app/api/routes/sessions.py b/multi_llm_chatbot_backend/app/api/routes/sessions.py new file mode 100644 index 0000000000000000000000000000000000000000..cf3c1a2869d63c5a4c88560cea278354019469e6 --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/sessions.py @@ -0,0 +1,265 @@ +from fastapi import APIRouter, Request, HTTPException, Depends +from app.core.session_manager import get_session_manager +from app.api.utils import get_or_create_session_for_request_async +from app.core.auth import get_current_active_user +from app.models.user import User +from pydantic import BaseModel +from typing import Optional +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter() +session_manager = get_session_manager() + +class ResetSessionRequest(BaseModel): + chat_session_id: Optional[str] = None + force_new: bool = False + +@router.get("/context") +async def get_context( + request: Request, + chat_session_id: Optional[str] = None, + current_user: User = Depends(get_current_active_user) +): + """ + Get context for current session - ENHANCED with document access fix + Now properly handles different chat sessions and ensures document access + """ + try: + # Determine which session to get context for with consistent session ID format + if chat_session_id: + # Getting context for a specific chat session - use consistent format + session_id = f"chat_{chat_session_id}" + logger.info(f"Getting context for specific chat session: {session_id}") + + # Ensure session is loaded in memory + if session_id not in session_manager.sessions: + logger.info(f"Chat session {session_id} not in memory, loading from database") + loaded_session_id = await get_or_create_session_for_request_async( + request, + chat_session_id=chat_session_id, + user_id=str(current_user.id) + ) + session_id = loaded_session_id + logger.info(f"Loaded session ID: {session_id}") + else: + # Getting context for current session + session_id = await get_or_create_session_for_request_async(request) + logger.info(f"Getting context for current session: {session_id}") + + session = session_manager.get_session(session_id) + rag_stats = session.get_rag_stats() + + # Enhanced logging for document access debugging + logger.info(f"Retrieved context for session {session_id}:") + logger.info(f" - Messages: {len(session.messages)}") + logger.info(f" - Documents: {rag_stats.get('total_documents', 0)}") + logger.info(f" - Chunks: {rag_stats.get('total_chunks', 0)}") + logger.info(f" - Uploaded files: {len(session.uploaded_files)}") + + # Log document details if available + if rag_stats.get('documents'): + for doc in rag_stats['documents']: + logger.info(f" - Available document: {doc.get('filename', 'unknown')} ({doc.get('chunks', 0)} chunks)") + + # Include session debugging info in response + context_response = { + "session_id": session_id, + "chat_session_id": chat_session_id, + "messages": session.messages, + "rag_info": { + "total_documents": rag_stats.get("total_documents", 0), + "total_chunks": rag_stats.get("total_chunks", 0), + "documents": rag_stats.get("documents", []) + }, + "context_stats": { + "message_count": len(session.messages), + "user_messages": len([m for m in session.messages if m.get('role') == 'user']), + "uploaded_files": session.uploaded_files, + "total_upload_size": session.total_upload_size, + "created_at": session.created_at.isoformat(), + "last_accessed": session.last_accessed.isoformat() + }, + # Add debugging info + "debug_info": { + "session_format": "chat_session" if chat_session_id else "new_session", + "session_in_memory": session_id in session_manager.sessions, + "document_access_working": rag_stats.get("total_documents", 0) > 0 + } + } + + return context_response + + except Exception as e: + logger.error(f"Error getting context for session_id {session_id if 'session_id' in locals() else 'unknown'}: {str(e)}") + logger.error(f"Chat session ID: {chat_session_id}") + import traceback + logger.error(f"Full traceback: {traceback.format_exc()}") + + return { + "session_id": session_id if 'session_id' in locals() else None, + "chat_session_id": chat_session_id, + "messages": [], + "rag_info": {"total_documents": 0, "total_chunks": 0, "documents": []}, + "context_stats": { + "message_count": 0, + "user_messages": 0, + "uploaded_files": [], + "total_upload_size": 0 + }, + "error": str(e), + "debug_info": { + "error_occurred": True, + "error_type": type(e).__name__ + } + } + +@router.post("/reset-session") +async def reset_session( + reset_request: ResetSessionRequest, + request: Request, + current_user: User = Depends(get_current_active_user) +): + """ + Reset session - ENHANCED + Now properly handles different reset scenarios + """ + try: + if reset_request.force_new: + # Force create a completely new session + session_id = session_manager.create_session() + session = session_manager.get_session(session_id) + session.clear_all_data() + + logger.info(f"Force created new session: {session_id}") + + return { + "status": "reset", + "message": "New session created with fresh context", + "session_id": session_id, + "chat_session_id": None + } + + elif reset_request.chat_session_id: + # Reset a specific chat session context + session_id = f"chat_{reset_request.chat_session_id}" + + if session_id in session_manager.sessions: + success = session_manager.reset_session_completely(session_id) + message = "Chat session context reset successfully" if success else "Failed to reset chat session context" + else: + # Create fresh context for this chat + session = session_manager.get_session(session_id) + session.clear_all_data() + success = True + message = "Fresh context created for chat session" + + logger.info(f"Reset chat session {reset_request.chat_session_id}, memory session: {session_id}") + + return { + "status": "reset" if success else "error", + "message": message, + "session_id": session_id, + "chat_session_id": reset_request.chat_session_id + } + + else: + # Reset current session + session_id = await get_or_create_session_for_request_async(request) + success = session_manager.reset_session_completely(session_id) + + logger.info(f"Reset current session: {session_id}") + + return { + "status": "reset" if success else "error", + "message": "Current session reset successfully" if success else "Failed to reset current session", + "session_id": session_id + } + + except Exception as e: + logger.error(f"Error resetting session: {e}") + return {"status": "error", "message": f"Failed to reset session: {str(e)}"} + +@router.get("/session-stats") +async def get_session_stats( + request: Request, + chat_session_id: Optional[str] = None, + current_user: User = Depends(get_current_active_user) +): + """ + Get session statistics - ENHANCED + Now provides detailed stats for different session types + """ + try: + if chat_session_id: + # Stats for specific chat session + session_id = f"chat_{chat_session_id}" + else: + # Stats for current session + session_id = await get_or_create_session_for_request_async(request) + + stats = session_manager.get_session_stats(session_id) + + # Add additional context + stats["session_type"] = "chat_session" if chat_session_id else "current_session" + stats["chat_session_id"] = chat_session_id + + return stats + + except Exception as e: + logger.error(f"Error getting session stats: {str(e)}") + return {"error": str(e)} + +@router.get("/active-sessions") +async def get_active_sessions(current_user: User = Depends(get_current_active_user)): + """ + Get all active sessions for debugging + """ + try: + active_count = session_manager.get_active_session_count() + + # Get overview of sessions (don't return full content for privacy) + session_overview = {} + for session_id, session in session_manager.sessions.items(): + session_overview[session_id] = { + "message_count": len(session.messages), + "uploaded_files": len(session.uploaded_files), + "created_at": session.created_at.isoformat(), + "last_accessed": session.last_accessed.isoformat(), + "is_chat_session": session_id.startswith("chat_") + } + + return { + "active_session_count": active_count, + "sessions": session_overview + } + + except Exception as e: + logger.error(f"Error getting active sessions: {str(e)}") + return {"error": str(e)} + +@router.post("/cleanup-sessions") +async def cleanup_expired_sessions(current_user: User = Depends(get_current_active_user)): + """ + Manually trigger session cleanup + """ + try: + initial_count = session_manager.get_active_session_count() + + # Force cleanup + session_manager._cleanup_expired_sessions() + + final_count = session_manager.get_active_session_count() + cleaned_count = initial_count - final_count + + return { + "status": "success", + "message": f"Cleaned up {cleaned_count} expired sessions", + "sessions_before": initial_count, + "sessions_after": final_count + } + + except Exception as e: + logger.error(f"Error during session cleanup: {str(e)}") + return {"status": "error", "message": str(e)} \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/api/routes/user_profile.py b/multi_llm_chatbot_backend/app/api/routes/user_profile.py new file mode 100644 index 0000000000000000000000000000000000000000..b64145289a176454b9cd8d75d5777146d5918c89 --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/user_profile.py @@ -0,0 +1,201 @@ +import logging +from datetime import datetime +from typing import Any, Dict, List, Optional + +from fastapi import APIRouter, Depends +from pydantic import BaseModel + +from app.core.auth import get_current_active_user +from app.core.database import get_database +from app.models.user import User +from app.models.user_profile import UserProfileResponse, UserProfileUpdate + +LOG = logging.getLogger(__name__) + +router = APIRouter() + +PROFILE_FIELDS = [ + "knowledge_level", + "timezone", + "cyber_role", + "organization_type", + "primary_domains", + "certifications", + "tools_stack", + "compliance_focus", + "current_goals", + "learning_preferences", +] + +LIST_FIELDS = {"primary_domains", "certifications", "tools_stack"} + +_SELECT_OPTIONS: Dict[str, List[str]] = { + # NOTE: field keys are kept generic (cyber_role / organization_type) for + # storage compatibility; the labels/options are themed for the panel. + "cyber_role": [ + "Internship search", + "Full-time / entry-level", + "Both internship and FT", + "Career exploration", + "Return offer conversion", + "Industry switch / pivot", + "Graduate school path", + "Other", + ], + "organization_type": [ + "Handshake", + "LinkedIn", + "Company career pages", + "Career fairs / campus recruiting", + "Referrals / alumni", + "Hybrid (multiple channels)", + ], +} + + +def _is_field_filled(value: Any) -> bool: + if value is None: + return False + if isinstance(value, str): + return bool(value.strip()) + if isinstance(value, list): + return len(value) > 0 + return bool(value) + + +def enrich_profile_from_user( + doc: Optional[Dict[str, Any]], user: User +) -> Dict[str, Any]: + """Merge account signup fields into the profile when profile slots are empty.""" + merged = dict(doc or {}) + if user.academicStage and not _is_field_filled(merged.get("knowledge_level")): + merged["knowledge_level"] = user.academicStage + if user.researchArea and not _is_field_filled(merged.get("timezone")): + merged["timezone"] = user.researchArea + return merged + + +def _calc_completion(doc: Dict[str, Any]) -> int: + filled = sum(1 for f in PROFILE_FIELDS if _is_field_filled(doc.get(f))) + return int(filled / len(PROFILE_FIELDS) * 100) + + +def _profile_response(doc: Dict[str, Any], user: User) -> UserProfileResponse: + enriched = enrich_profile_from_user(doc, user) + fields = {k: _normalize_field(k, enriched.get(k)) for k in PROFILE_FIELDS} + return UserProfileResponse( + user_id=str(enriched.get("user_id", user.id)), + **fields, + advisor_notes=enriched.get("advisor_notes"), + updated_at=enriched.get("updated_at"), + completion_pct=_calc_completion(enriched), + ) + + +_SELECT_LOOKUP: Dict[str, Dict[str, str]] = { + field: {opt.lower(): opt for opt in opts} + for field, opts in _SELECT_OPTIONS.items() +} + + +def _normalize_select(key: str, value: str) -> str: + lookup = _SELECT_LOOKUP.get(key) + if not lookup or not isinstance(value, str): + return value + v = value.strip() + if v.lower() in lookup: + return lookup[v.lower()] + for canonical_lower, canonical in lookup.items(): + if canonical_lower in v.lower(): + return canonical + return v + + +def _normalize_field(key: str, value: Any) -> Any: + if key in LIST_FIELDS: + if isinstance(value, str): + return [s.strip() for s in value.split(",") if s.strip()] + if isinstance(value, list): + return value + return [] + if key in _SELECT_OPTIONS: + return _normalize_select(key, value) + if isinstance(value, list): + return ", ".join(str(v) for v in value if v) + return value + + +async def _sync_profile_to_user(user: User, update_data: Dict[str, Any]) -> None: + """Keep users.academicStage / researchArea aligned with profile edits.""" + user_updates: Dict[str, Any] = {} + if "knowledge_level" in update_data: + user_updates["academicStage"] = update_data["knowledge_level"] or None + if "timezone" in update_data: + user_updates["researchArea"] = update_data["timezone"] or None + if user_updates: + db = get_database() + await db.users.update_one({"_id": user.id}, {"$set": user_updates}) + + +@router.get("/users/me/profile", response_model=UserProfileResponse) +async def get_my_profile( + current_user: User = Depends(get_current_active_user), +) -> UserProfileResponse: + db = get_database() + doc = await db.user_profiles.find_one({"user_id": current_user.id}) + if not doc: + doc = {"user_id": current_user.id} + return _profile_response(doc, current_user) + + +@router.put("/users/me/profile", response_model=UserProfileResponse) +async def update_my_profile( + updates: UserProfileUpdate, + current_user: User = Depends(get_current_active_user), +) -> UserProfileResponse: + db = get_database() + update_data = {k: v for k, v in updates.dict().items() if v is not None} + update_data["updated_at"] = datetime.utcnow() + await db.user_profiles.update_one( + {"user_id": current_user.id}, + {"$set": update_data, "$setOnInsert": {"user_id": current_user.id}}, + upsert=True, + ) + await _sync_profile_to_user(current_user, update_data) + doc = await db.user_profiles.find_one({"user_id": current_user.id}) or { + "user_id": current_user.id + } + return _profile_response(doc, current_user) + + +class ClearDataRequest(BaseModel): + profile: bool = False + chats: bool = False + canvas: bool = False + + +@router.post("/users/me/clear-data") +async def clear_user_data( + req: ClearDataRequest, + current_user: User = Depends(get_current_active_user), +) -> Dict[str, List[str]]: + db = get_database() + cleared: List[str] = [] + + if req.profile: + await db.user_profiles.delete_many({"user_id": current_user.id}) + await db.onboarding_conversations.delete_many({"user_id": current_user.id}) + cleared.append("profile") + + if req.chats: + result = await db.chat_sessions.update_many( + {"user_id": current_user.id, "is_active": True}, + {"$set": {"is_active": False, "updated_at": datetime.utcnow()}}, + ) + cleared.append(f"chats ({result.modified_count})") + + if req.canvas: + await db.phd_canvases.delete_many({"user_id": str(current_user.id)}) + cleared.append("canvas") + + return {"cleared": cleared} diff --git a/multi_llm_chatbot_backend/app/api/routes/voice.py b/multi_llm_chatbot_backend/app/api/routes/voice.py new file mode 100644 index 0000000000000000000000000000000000000000..2a32ba92ec0b1b87c54673bc8b4a0885ceaa2870 --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/routes/voice.py @@ -0,0 +1,270 @@ +import asyncio +import html as html_module +import logging +import re +import struct +import subprocess +import tempfile +from pathlib import Path +from typing import Dict, List, Optional +from urllib.parse import quote + +import httpx +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile +from fastapi.responses import Response +from pydantic import BaseModel + +from app.core.auth import get_current_active_user +from app.models.user import User +from app.config import get_settings + +logger = logging.getLogger(__name__) + +router = APIRouter() + +voice_settings = get_settings().voice +TTS_BASE = voice_settings.tts_endpoint +STT_BASE = voice_settings.stt_endpoint + +HTTP_TIMEOUT = 120 +# Coqui uses GET /synthesize/{text}; keep chunks small for URL limits. +MAX_SYNTH_CHUNK = 200 + +_SECTION_HEADERS = re.compile( + r"\b(Thought|What to do|Next step)\s*[:.]?\s*", flags=re.IGNORECASE +) + + +class TTSRequest(BaseModel): + text: str + + +def _rough_spoken_text(raw: str) -> str: + """Light cleanup so model markdown is tolerable for TTS (no extra deps).""" + t = raw.strip() + t = re.sub(r"```[\s\S]*?```", " ", t) + t = re.sub(r"`([^`]+)`", r"\1", t) + t = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", t) + t = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"\1", t) + t = re.sub(r"^#{1,6}\s+", "", t, flags=re.MULTILINE) + t = re.sub(r"[*_]{1,3}", "", t) + t = html_module.unescape(t) + t = _SECTION_HEADERS.sub(" ", t) + t = re.sub(r"\s+", " ", t).strip() + return t + + +def _text_chunks(text: str, max_len: int = MAX_SYNTH_CHUNK) -> List[str]: + text = text.strip() + if not text: + return [] + out: List[str] = [] + while text: + if len(text) <= max_len: + out.append(text) + break + cut = text.rfind(" ", 0, max_len) + if cut <= 0: + cut = max_len + piece = text[:cut].strip() + if piece: + out.append(piece) + text = text[cut:].strip() + return out + + +async def _synthesize_one(client: httpx.AsyncClient, chunk: str) -> Optional[bytes]: + url = f"{TTS_BASE}/synthesize/{quote(chunk, safe='')}" + try: + r = await client.get(url) + r.raise_for_status() + return r.content + except Exception as exc: + logger.warning("TTS chunk failed (%s chars): %s", len(chunk), exc) + return None + + +def _concat_wav(segments: List[bytes]) -> bytes: + if len(segments) == 1: + return segments[0] + + pcm_parts: List[bytes] = [] + first_header = b"" + + for i, seg in enumerate(segments): + # PCM WAV files begin with a 44-byte header; if the segment is shorter, + # then it cannot be valid WAV audio + if len(seg) < 44: + continue + if i == 0: + first_header = seg[:44] + data_offset = 44 + idx = seg.find(b"data") + if idx != -1 and idx + 8 <= len(seg): + data_offset = idx + 8 + pcm_parts.append(seg[data_offset:]) + + if not pcm_parts or not first_header: + return segments[0] if segments else b"" + + all_pcm = b"".join(pcm_parts) + pcm_len = len(all_pcm) + + header = bytearray(first_header) + struct.pack_into(" bytes: + with tempfile.TemporaryDirectory() as tmp: + ext = "webm" if "webm" in (src_mime or "") else "ogg" + src = Path(tmp) / f"in.{ext}" + dst = Path(tmp) / "out.wav" + src.write_bytes(audio_bytes) + result = subprocess.run( + [ + "ffmpeg", + "-y", + "-i", + str(src), + "-ar", + "16000", + "-ac", + "1", + "-f", + "wav", + str(dst), + ], + capture_output=True, + timeout=30, + ) + if result.returncode != 0: + logger.warning( + "ffmpeg stderr: %s", + result.stderr.decode(errors="replace")[-500:], + ) + raise RuntimeError("ffmpeg conversion failed") + return dst.read_bytes() + + +@router.get("/voice/status") +async def voice_status( + current_user: User = Depends(get_current_active_user), +) -> Dict[str, bool]: + tts_ready = False + stt_ready = False + async with httpx.AsyncClient(timeout=5) as client: + if TTS_BASE not in ("", None): + try: + resp = await client.get(f"{TTS_BASE}/status") + tts_ready = resp.status_code == 200 + except Exception: + tts_ready = False + if STT_BASE not in ("", None): + try: + resp = await client.get(f"{STT_BASE}/status") + stt_ready = resp.status_code == 200 + except Exception: + stt_ready = False + return {"tts_ready": tts_ready, "stt_ready": stt_ready} + + +@router.post("/voice/transcribe") +async def transcribe_audio( + audio: UploadFile = File(...), + current_user: User = Depends(get_current_active_user), +) -> Dict[str, str]: + """ + Transcribe an uploaded audio file and return the text. + @param audio: The uploaded audio file (from a browser recording) + @param current_user: The authenticated user making the request + @return: A dictionary with the transcribed text under key "text" + """ + contents = await audio.read() + if not contents: + return {"text": ""} + + mime = audio.content_type or "audio/webm" + logger.info("STT: received %s bytes (%s)", len(contents), mime) + + need_convert = "wav" not in mime.lower() + if need_convert: + try: + loop = asyncio.get_running_loop() + wav_bytes = await loop.run_in_executor( + None, _convert_to_wav, contents, mime + ) + logger.info("STT: converted to WAV (%s bytes)", len(wav_bytes)) + except Exception as e: + logger.error("STT conversion error: %s", e) + raise HTTPException(status_code=500, detail="Audio conversion failed") + else: + wav_bytes = contents + + try: + async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client: + resp = await client.post( + f"{STT_BASE}/stt", + content=wav_bytes, + headers={"Content-Type": "audio/wav"}, + ) + resp.raise_for_status() + text = resp.text.strip().strip('"') + logger.info("STT result: %r", text[:100]) + return {"text": text} + except httpx.TimeoutException: + raise HTTPException(status_code=504, detail="STT service timed out") + except Exception as e: + logger.error("STT proxy error: %s", e) + raise HTTPException(status_code=502, detail="STT service unavailable") + + +@router.post("/voice/tts") +async def text_to_speech( + req: TTSRequest, + current_user: User = Depends(get_current_active_user), +) -> Response: + """ + Generate spoken audio for the input text + @param req: A TTSRequest containing the text to synthesize + @param current_user: The authenticated user making the request + @return: A Response containing the synthesized audio in WAV format + """ + raw = req.text.strip() + if not raw: + raise HTTPException(status_code=400, detail="Text is required") + + if len(raw) > 5000: + raw = raw[:5000] + + spoken = _rough_spoken_text(raw) + chunks = _text_chunks(spoken) + logger.info( + "TTS: %s chunk(s) from %s input chars โ†’ %s spoken chars", + len(chunks), + len(req.text), + len(spoken), + ) + + try: + async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client: + results = await asyncio.gather( + *[_synthesize_one(client, c) for c in chunks] + ) + wav_segments = [r for r in results if r and len(r) > 44] + + if not wav_segments: + raise HTTPException( + status_code=502, detail="TTS synthesis failed for all chunks" + ) + + combined = _concat_wav(wav_segments) + return Response(content=combined, media_type="audio/wav") + except HTTPException: + raise + except httpx.TimeoutException: + raise HTTPException(status_code=504, detail="TTS service timed out") + except Exception as e: + logger.error("TTS proxy error: %s", e) + raise HTTPException(status_code=502, detail="TTS service unavailable") diff --git a/multi_llm_chatbot_backend/app/api/utils.py b/multi_llm_chatbot_backend/app/api/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e816f395c2f0cc684da3df3581c9d7d1ac1645bb --- /dev/null +++ b/multi_llm_chatbot_backend/app/api/utils.py @@ -0,0 +1,140 @@ +from typing import Optional +from fastapi import Request +from app.core.session_manager import get_session_manager +from app.core.database import get_database +from bson import ObjectId +import logging + +logger = logging.getLogger(__name__) +session_manager = get_session_manager() + +async def load_chat_session_into_context(chat_session_id: str, user_id: str) -> str: + """ + Load a chat session from MongoDB into memory context - ENHANCED DEBUG VERSION + """ + try: + db = get_database() + + # Add enhanced debugging + logger.info(f"=== LOADING CHAT SESSION DEBUG ===") + logger.info(f"Attempting to load chat_session_id: {chat_session_id}") + logger.info(f"For user_id: {user_id}") + + # Try to find the session with enhanced debugging + chat_session = await db.chat_sessions.find_one({ + "_id": ObjectId(chat_session_id), + "user_id": ObjectId(user_id), + "deleted_at": {"$exists": False} + }) + + if not chat_session: + logger.warning(f"Chat session {chat_session_id} not found for user {user_id}") + + # Debug: Check if session exists for any user + try: + session_exists = await db.chat_sessions.find_one({"_id": ObjectId(chat_session_id)}) + if session_exists: + logger.warning(f"Session exists but for different user: {session_exists.get('user_id')}") + logger.warning(f"Expected user: {user_id}") + logger.warning(f"Session user: {session_exists.get('user_id')}") + logger.warning(f"User ID types - Expected: {type(user_id)}, Found: {type(session_exists.get('user_id'))}") + else: + logger.warning(f"Session {chat_session_id} does not exist in database at all") + except Exception as debug_error: + logger.error(f"Error during session debug: {debug_error}") + + # Debug: List recent sessions for this user + try: + recent_sessions = await db.chat_sessions.find( + {"user_id": ObjectId(user_id), "deleted_at": {"$exists": False}} + ).limit(5).to_list(5) + logger.info(f"Recent sessions for user {user_id}: {[str(s['_id']) for s in recent_sessions]}") + except Exception as debug_error: + logger.error(f"Error listing recent sessions: {debug_error}") + + return None + + logger.info(f"โœ… Found chat session: {chat_session.get('title', 'Untitled')}") + logger.info(f"โœ… Message count: {len(chat_session.get('messages', []))}") + + # Create consistent memory session ID + memory_session_id = f"chat_{chat_session_id}" + logger.info(f"โœ… Creating memory session: {memory_session_id}") + + # Get session manager and create memory session + session_manager = get_session_manager() + memory_session = session_manager.get_session(memory_session_id) + + # Clear any existing data + memory_session.clear_all_data() + + # Load messages into memory session + messages = chat_session.get('messages', []) + for msg_data in messages: + try: + msg_type = msg_data.get('type', '') + if msg_type == 'user': + role = 'user' + elif msg_type == 'advisor': + role = msg_data.get('persona_id') or 'assistant' + else: + role = 'assistant' + message = { + 'id': msg_data.get('id', 'unknown'), + 'role': role, + 'content': msg_data.get('content', ''), + 'timestamp': msg_data.get('timestamp', '') + } + memory_session.append_message(message['role'], message['content']) + + # Store original message for export + if not hasattr(memory_session, 'original_messages'): + memory_session.original_messages = [] + + memory_session.original_messages.append(message) + except Exception as msg_error: + logger.error(f"Error loading message: {msg_error}") + continue + + logger.info(f"Loaded {len(messages)} messages into session {memory_session_id}") + return memory_session_id + + except Exception as e: + logger.error(f"Error loading chat session into context: {e}") + import traceback + logger.error(f"Full traceback: {traceback.format_exc()}") + return None + +async def get_or_create_session_for_request_async( + request: Request, + session_id_override: Optional[str] = None, + chat_session_id: Optional[str] = None, + user_id: Optional[str] = None +) -> str: + """ + Async version that properly handles chat session loading + """ + + # Case 1: Loading an existing chat session + if chat_session_id and user_id: + memory_session_id = f"chat_{chat_session_id}" + loaded = await load_chat_session_into_context(chat_session_id, user_id) + if loaded: + return loaded + # Keep a stable session id even when MongoDB load fails (e.g. race on first message) + session_manager.get_session(memory_session_id) + return memory_session_id + + # Case 2: Explicit session ID provided + if session_id_override: + return session_id_override + + # Case 3: Check for session header + session_header = request.headers.get("X-Session-ID") + if session_header: + return session_header + + # Case 4: Create a truly new session + new_session_id = session_manager.create_session() + logger.info(f"Created new session: {new_session_id}") + return new_session_id \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/assets/avatars/advisor1.png b/multi_llm_chatbot_backend/app/assets/avatars/advisor1.png new file mode 100644 index 0000000000000000000000000000000000000000..ac03b8d64e4d8ac25609c34c950a1cd00a0bac34 --- /dev/null +++ b/multi_llm_chatbot_backend/app/assets/avatars/advisor1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8def49b76592eb14f37b308542534720dd5df516fe8c9b95dff00f2534f51727 +size 5792062 diff --git a/multi_llm_chatbot_backend/app/assets/avatars/advisor2.png b/multi_llm_chatbot_backend/app/assets/avatars/advisor2.png new file mode 100644 index 0000000000000000000000000000000000000000..53108576b3123bb67aacb3990038ce9130fb2979 --- /dev/null +++ b/multi_llm_chatbot_backend/app/assets/avatars/advisor2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:676f77e3d01c0a905658e965be55f3b6d3a91370debc1ba930f9e5a7ed7a80a2 +size 5326706 diff --git a/multi_llm_chatbot_backend/app/assets/avatars/advisor3.png b/multi_llm_chatbot_backend/app/assets/avatars/advisor3.png new file mode 100644 index 0000000000000000000000000000000000000000..85d5ce3016022af88fcfed804a5425f4c38944ab --- /dev/null +++ b/multi_llm_chatbot_backend/app/assets/avatars/advisor3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e4e59ac55e3bc490f36b5f85b03c8634ebe1ae7727e888093c4b7e1879c4473 +size 5573017 diff --git a/multi_llm_chatbot_backend/app/assets/avatars/advisor4.png b/multi_llm_chatbot_backend/app/assets/avatars/advisor4.png new file mode 100644 index 0000000000000000000000000000000000000000..831b011494d59aa693498e9eec84b06cca94f490 --- /dev/null +++ b/multi_llm_chatbot_backend/app/assets/avatars/advisor4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b97c9f62b1cfb8d4ce6e57c187dbad6a6fab69b24e446af31dec8999ce800306 +size 5326776 diff --git a/multi_llm_chatbot_backend/app/assets/avatars/advisor5.png b/multi_llm_chatbot_backend/app/assets/avatars/advisor5.png new file mode 100644 index 0000000000000000000000000000000000000000..5dd4b66cb7a3c1985cbb70f7feaec6f58194ad90 --- /dev/null +++ b/multi_llm_chatbot_backend/app/assets/avatars/advisor5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:769b6a6461cdb2f92e3e71fbbcd2b1361b560fd7c49617f3a476eaf1a6bf9199 +size 5042882 diff --git a/multi_llm_chatbot_backend/app/assets/avatars/advisor6.png b/multi_llm_chatbot_backend/app/assets/avatars/advisor6.png new file mode 100644 index 0000000000000000000000000000000000000000..608f527fd077023e3f8cddee9568db07009a1a1a --- /dev/null +++ b/multi_llm_chatbot_backend/app/assets/avatars/advisor6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d066f008dffcb0332642e35e3f4805ed30598c03055ac149ab9c5c2892f1c8b +size 5299628 diff --git a/multi_llm_chatbot_backend/app/assets/avatars/advisor7.png b/multi_llm_chatbot_backend/app/assets/avatars/advisor7.png new file mode 100644 index 0000000000000000000000000000000000000000..2dffedb495ed70920466b6f7aa732cd644b3a342 --- /dev/null +++ b/multi_llm_chatbot_backend/app/assets/avatars/advisor7.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7ef861863ee750e201e7ee5eb483494b7dbadc975d0f2fb5cb9fdc0f428e0e6 +size 4280063 diff --git a/multi_llm_chatbot_backend/app/config.py b/multi_llm_chatbot_backend/app/config.py new file mode 100644 index 0000000000000000000000000000000000000000..cbedbfa1e9c7bc38cf2d6d908eb097937ed189c4 --- /dev/null +++ b/multi_llm_chatbot_backend/app/config.py @@ -0,0 +1,519 @@ +""" +Centralized application configuration. + +Reads ``config.yaml`` from the project root (two levels above this file) and +validates it with Pydantic models. Every setting falls back to environment +variables when the YAML value is empty, so existing ``.env`` workflows keep +working. +""" + +import os +import logging +import colorsys +from pathlib import Path +from typing import Any, Dict, List, Optional +from colorhash import ColorHash + +import httpx +import yaml +from pydantic import BaseModel, validator, Field, model_validator + +from app.utils.avatar_helpers import get_bundled_avatar_path + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Pydantic models +# --------------------------------------------------------------------------- + +class _IconValidatorMixin(BaseModel): + """Validates that the ``icon`` field is a known Lucide icon name.""" + + @model_validator(mode="after") + def _validate_icon(self): + from app.utils.lucide_icons import get_valid_icon_names + + valid = get_valid_icon_names() + if valid and self.icon not in valid: + raise ValueError( + f"Unknown icon {self.icon!r}. " + f"Must be a valid Lucide icon name." + ) + return self + + +class FeatureConfig(_IconValidatorMixin): + title: str = "" + description: str = "" + icon: str = "HelpCircle" + + +class UserAvatarOption(BaseModel): + id: str + icon: str = "User" + color: str = "#2563EB" + bg: str = "#EFF6FF" + + +class AppConfig(BaseModel): + title: str = "Advisor Canvas" + subtitle: str = "AI-Powered Guidance" + primary_color: str = "#7C3AED" + logo_icon: str = "Users" + footer_text: str = "" + user_avatars: List[UserAvatarOption] = [] + + +class HomepageConfig(BaseModel): + headline_prefix: str = "Get Guidance from" + headline_highlight: str = "Advisor Personas" + description: str = "" + features_title: str = "Why Choose Our Advisory Panel?" + features: List[FeatureConfig] = [] + + +class AcademicStage(BaseModel): + value: str = "" + label: str = "" + + +class LoginConfig(BaseModel): + subtitle: str = "Sign in to continue" + signup_subtitle: str = "Create your account to get personalized guidance from expert advisors" + academic_stages: List[AcademicStage] = [] + knowledge_levels: List[AcademicStage] = [] + timezones: List[AcademicStage] = [] + + +class ExampleCategory(_IconValidatorMixin): + title: str + icon: str = "BookOpen" + color: str = "#3B82F6" + bg_color: str = "#EFF6FF" + suggestions: List[str] = [] + + +class ChatPageConfig(BaseModel): + placeholder: str = "Ask your advisors anything..." + examples: List[ExampleCategory] = [] + + +class PersonaItemConfig(_IconValidatorMixin): + id: str + name: str + enabled: bool = True + role: str = "" + summary: str = "" + color: Optional[str] = None + bg_color: Optional[str] = None + dark_color: Optional[str] = None + dark_bg_color: Optional[str] = None + icon: str = "HelpCircle" + avatar: Optional[str] = None + temperature: int = 5 + persona_prompt: str = "" + + @model_validator(mode='after') + def _auto_generate_colors(self): + if self.color is None: + generated = generate_persona_colors(self.name) + self.color = generated["color"] + self.bg_color = generated["bg_color"] + self.dark_color = generated["dark_color"] + self.dark_bg_color = generated["dark_bg_color"] + return self + + def _resolve_image(self) -> str: + """Resolve the persona's visual representation as a URI string. + + Returns the avatar as a URI the frontend can dispatch on by scheme: + + - ``https://โ€ฆ`` / ``http://โ€ฆ`` - external image URL + - ``/api/avatars/bundled/โ€ฆ`` - server-relative path for a bundled file + - ``icon://`` - render a Lucide icon component + + Falls back to ``icon://`` when a bundled avatar name doesn't match a + file on disk or when an external URL is unreachable. + """ + if self.avatar is None: + return f"icon://{self.icon}" + if self.avatar.startswith(("http://", "https://")): + try: + resp = httpx.head(self.avatar, timeout=5, follow_redirects=True) + if resp.is_success: + return self.avatar + logger.warning( + "Avatar URL %r returned status %d for persona %r, falling back to icon.", + self.avatar, resp.status_code, self.id, + ) + except httpx.HTTPError as exc: + logger.warning( + "Avatar URL %r unreachable for persona %r (%s), falling back to icon.", + self.avatar, self.id, exc, + ) + return f"icon://{self.icon}" + if get_bundled_avatar_path(self.avatar) is None: + logger.warning( + "Bundled avatar %r not found for persona %r, falling back to icon.", + self.avatar, self.id, + ) + return f"icon://{self.icon}" + # Default to empty string (โ†’ relative URL) so single-origin Spaces + # deployments serve avatars off the same host as the SPA. Local + # ``npm start`` development setups can still set REACT_APP_API_URL + # explicitly to point at the backend on a different port. + base = os.getenv("REACT_APP_API_URL", "").rstrip("/") + return f"{base}/api/avatars/bundled/{self.avatar}" + + def to_frontend_config(self) -> dict: + return { + "id": self.id, + "name": self.name, + "role": self.role, + "summary": self.summary, + "color": self.color, + "bg_color": self.bg_color, + "dark_color": self.dark_color, + "dark_bg_color": self.dark_bg_color, + "image": self._resolve_image(), + } + + +class PersonasConfig(BaseModel): + base_prompt: str = "" + personas_dir: str = "" + config_dir: str = "" + items: List[PersonaItemConfig] = [] + + @model_validator(mode='after') + def _load_personas_from_directory(self): + if self.personas_dir: + dir_path = Path(self.personas_dir) + if not dir_path.is_absolute() and self.config_dir: + dir_path = Path(self.config_dir) / dir_path + loaded = load_personas_from_dir(str(dir_path)) + if loaded: + self.items = loaded + logger.info(f"Loaded {len(loaded)} personas.") + else: + logger.warning(f"No personas found in {self.personas_dir}. falling back to personas.items config") + return self + + +class OrchestratorConfig(BaseModel): + min_words_without_keywords: int = 6 + conversation_history_token_threshold: int = 4000 + specific_keywords: List[str] = [] + clarification_questions: List[str] = [ + "Could you provide more details about what you need help with?"] + clarification_suggestions: List[str] = [ + "Provide more details about your question."] + + @model_validator(mode="after") + def validate_clarificaiton_questions(self): + if len(self.clarification_questions) < 1: + raise ValueError("At least one clarification question is required.") + return self + + +def resolve_jwt_secret(explicit: str = "") -> str: + """Resolve a stable JWT signing secret across restarts. + + Precedence: config value โ†’ JWT_SECRET_KEY env โ†’ persisted file under + DATA_DIR (generated on first run) โ†’ insecure dev fallback. + """ + if explicit and explicit.strip(): + return explicit.strip() + + env_secret = os.getenv("JWT_SECRET_KEY", "").strip() + if env_secret: + return env_secret + + data_dir = Path(os.environ.get("DATA_DIR") or Path.home() / "data") + secret_path = data_dir / "jwt_secret" + try: + data_dir.mkdir(parents=True, exist_ok=True) + if secret_path.is_file(): + stored = secret_path.read_text(encoding="utf-8").strip() + if stored: + return stored + import secrets + + generated = secrets.token_urlsafe(48) + secret_path.write_text(generated, encoding="utf-8") + logger.info("Generated persistent JWT secret at %s", secret_path) + return generated + except OSError as exc: + logger.warning( + "Cannot persist JWT secret at %s (%s); using insecure default. " + "Set JWT_SECRET_KEY or auth.jwt_secret for production.", + secret_path, + exc, + ) + return "your-secret-key-change-me" + + +class AuthConfig(BaseModel): + jwt_secret: str = Field(default=os.getenv("JWT_SECRET_KEY", "")) + algorithm: str = "HS256" + token_expiry_minutes: int = 43200 # 30 days + + @model_validator(mode="after") + def _validate_jwt_secret(self): + self.jwt_secret = resolve_jwt_secret(self.jwt_secret) + return self + + +class MongoDBConfig(BaseModel): + connection_string: str = Field(default=os.getenv("MONGODB_CONNECTION_STRING")) + database_name: str = "phd_advisor" + + @model_validator(mode="after") + def _warn_connection_envvar(self): + if os.getenv("MONGODB_CONNECTION_STRING"): + if self.connection_string != os.getenv("MONGODB_CONNECTION_STRING"): + logger.warning( + "MONGODB_CONNECTION_STRING envvar is overridden in " + "config.yaml" + ) + else: + logger.warning( + "MongoDB connection string not set in config.yaml. " + "Falling back to MONGODB_CONNECTION_STRING envvar." + ) + return self + + +class GeminiConfig(BaseModel): + api_key: str = Field(default=os.getenv("GEMINI_API_KEY")) + model: str = "gemini-2.5-flash" + + @model_validator(mode="after") + def _warn_gemini_envvar(self): + if os.getenv("GEMINI_API_KEY"): + if self.api_key != os.getenv("GEMINI_API_KEY"): + logger.warning( + "GEMINI_API_KEY envvar is overridden in config.yaml" + ) + else: + logger.warning( + "Gemini API key not set in config.yaml. " + "Falling back to GEMINI_API_KEY environment variable." + ) + return self + + +class OllamaConfig(BaseModel): + model: str = "llama3.2:1b" + # TODO: Drop support for `OLLAMA_BASE_URL` envvar handling + base_url: str = Field(default=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")) + + +class VllmConfig(BaseModel): + api_url: str = "" + api_key: str = Field(default=os.getenv("VLLM_API_KEY", "")) + api_username: str = Field(default=os.getenv("VLLM_API_USERNAME", "")) + model_id: str = "" + neon_persona_orchestrator: str = "vanilla" + neon_persona_advisors: str = "CybersecurityExpert" + + +class OpenAIConfig(BaseModel): + api_key: str = Field(default=os.getenv("OPENAI_API_KEY", "")) + model: str = "gpt-5.4" + orchestrator_reasoning_effort: str = "low" + persona_reasoning_effort: str = "none" + + +class ResilientConfig(BaseModel): + race_timeout_seconds: float = 3.0 + + +class LLMConfig(BaseModel): + provider: str = "gemini" + gemini: GeminiConfig = GeminiConfig() + ollama: OllamaConfig = OllamaConfig() + vllm: VllmConfig = VllmConfig() + openai: OpenAIConfig = OpenAIConfig() + resilient: ResilientConfig = ResilientConfig() + + +class RAGConfig(BaseModel): + embedding_model: str = "all-MiniLM-L6-v2" + chroma_collection: str = "phd_advisor_documents" + + +class ToolsConfig(BaseModel): + model_config = {"extra": "allow"} + + def get_enabled_names(self) -> List[str]: + """Return tool names whose config has ``enabled: true``.""" + return [ + name + for name, cfg in self.__pydantic_extra__.items() + if isinstance(cfg, dict) and cfg.get("enabled", True) + ] + + def get_tool_config(self, name: str) -> Dict[str, Any]: + """Return the raw config dict for a single tool, or ``{}``.""" + cfg = self.__pydantic_extra__.get(name, {}) + return cfg if isinstance(cfg, dict) else {} + + +class VoiceConfig(BaseModel): + stt_endpoint: str = "https://whisper.neonaiservices.com" + tts_endpoint: str = "https://coqui.neonaiservices.com" + + +class AppSettings(BaseModel): + """Top-level container that mirrors the YAML structure.""" + app: AppConfig = AppConfig() + homepage: HomepageConfig = HomepageConfig() + login: LoginConfig = LoginConfig() + chat_page: ChatPageConfig = ChatPageConfig() + personas: PersonasConfig = PersonasConfig() + orchestrator: OrchestratorConfig = OrchestratorConfig() + auth: AuthConfig = AuthConfig() + mongodb: MongoDBConfig = MongoDBConfig() + llm: LLMConfig = LLMConfig() + rag: RAGConfig = RAGConfig() + tools: ToolsConfig = ToolsConfig() + voice: VoiceConfig = VoiceConfig() + + # ------------------------------------------------------------------ + # Convenience helpers + # ------------------------------------------------------------------ + + def get_frontend_config(self) -> dict: + """Return the subset of configuration safe to expose to the frontend + via ``GET /api/config``. Secrets are excluded.""" + return { + "app": self.app.dict(), + "homepage": self.homepage.dict(), + "login": self.login.dict(), + "chat_page": self.chat_page.dict(), + "personas": { + "items": [p.to_frontend_config() for p in self.personas.items], + }, + } + + +# --------------------------------------------------------------------------- +# Singleton loader +# --------------------------------------------------------------------------- + +_settings: Optional[AppSettings] = None + + +def load_settings(config_path: Optional[str] = None) -> AppSettings: + """Load and validate ``config.yaml``, returning an ``AppSettings`` object. + + The result is cached as a module-level singleton so subsequent calls are + free. Pass *config_path* to override the auto-detected location (useful + for tests). + """ + global _settings + if _settings is not None: + return _settings + + config_path = config_path or os.getenv("CONFIG_PATH") + if not config_path: + logger.warning("No CONFIG_PATH specified. Using default values") + raw = {} + else: + path = Path(config_path) + if not path.exists(): + raise FileNotFoundError(f"Configuration file not found at {config_path}") + logger.info(f"Loading configuration from {path}") + with open(path, "r", encoding="utf-8") as fh: + raw = yaml.safe_load(fh) or {} + + personas_cfg = raw.setdefault("personas", {}) + + if config_path: + personas_cfg["config_dir"] = str(Path(config_path).parent) + + _settings = AppSettings(**raw) + logger.info(f"Configuration loaded: app.title={_settings.app.title}") + return _settings + + +def get_settings() -> AppSettings: + """Return the cached settings singleton (loads on first call).""" + return load_settings() + + +# --------------------------------------------------------------------------- +# Helper Functions +# --------------------------------------------------------------------------- + + +def load_personas_from_dir(personas_dir: str) -> List[PersonaItemConfig]: + """Load persona configs from individual YAML files in a directory. + + Each file is validated independently โ€” invalid files are skipped with a + warning. Duplicate ids/names and disabled personas are filtered out. + """ + + dir_path = Path(personas_dir) + if not dir_path.is_dir(): + logger.warning(f"Personas directory not found: {personas_dir}") + return [] + + personas: List[PersonaItemConfig] = [] + seen_ids: dict[str, str] = {} # id -> filename that defined it + seen_names: dict[str, str] = {} # name -> filename that defined it + + # sorting files alphabetically ensures consistent and predictable loading order + for filepath in sorted(dir_path.glob("*.yaml")): + try: + with open(filepath, "r", encoding="utf-8") as fh: + raw = yaml.safe_load(fh) or {} + persona = PersonaItemConfig(**raw) + except Exception as exc: + logger.warning(f"Skipping invalid persona file {filepath.name}: {exc}") + continue + + if not persona.enabled: + logger.info(f"Persona '{persona.id}' is disabled, skipping") + continue + + if persona.id in seen_ids: + logger.warning( + f"Duplicate persona id '{persona.id}' in {filepath.name} " + f"(already defined in {seen_ids[persona.id]}), skipping" + ) + continue + + if persona.name in seen_names: + logger.warning( + f"Duplicate persona name '{persona.name}' in {filepath.name} " + f"(already defined in {seen_names[persona.name]}), skipping" + ) + continue + + seen_ids[persona.id] = filepath.name + seen_names[persona.name] = filepath.name + personas.append(persona) + + logger.info(f"Loaded {len(personas)} persona(s) from {personas_dir}") + return personas + + +def generate_persona_colors(name: str) -> dict: + """Deterministically generate four theme colors from a persona name.""" + + ch = ColorHash(name.lower(), lightness=[0.55], saturation=[0.65]) + hue = ch.hsl[0] # grab the hue colorhash picked + h = hue / 360 + + def hsl_to_hex(h, s, l): + r, g, b = colorsys.hls_to_rgb(h, l, s) + return f"#{int(r*255):02X}{int(g*255):02X}{int(b*255):02X}" + + return { + "color": hsl_to_hex(h, 0.65, 0.55), + "bg_color": hsl_to_hex(h, 0.60, 0.95), + "dark_color": hsl_to_hex(h, 0.70, 0.70), + "dark_bg_color": hsl_to_hex(h, 0.65, 0.25), + } diff --git a/multi_llm_chatbot_backend/app/core/README.md b/multi_llm_chatbot_backend/app/core/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5383d98b8c59276dafc820eee93e3ef7515b165a --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/README.md @@ -0,0 +1,200 @@ +# `app/core` โ€“ Application Core Logic + +This is the **central brain** of the multi-LLM chatbot system. It orchestrates user interaction, persona logic, context management, document-based retrieval (RAG), session tracking, authentication, and initialization. + +--- + +## Overview of Modules + +| Module | Responsibility | +|--------|----------------| +| `auth.py` | Authentication (JWT, password hashing, user resolution) | +| `bootstrap.py` | System startup logic: loads LLMs, personas, orchestrators | +| `context.py` | Global per-session context (simplified storage) | +| `context_manager.py` | Core context formatting & windowing for Gemini/Ollama | +| `database.py` | MongoDB connection & index management | +| `improved_orchestrator.py` | Main message routing engine: document-aware, multi-persona orchestrator | +| `rag_manager.py` | RAG with ChromaDB: chunking, storage, semantic search | +| `session_manager.py` | Full chat lifecycle tracker (in-memory) with RAG hooks | + +--- + +## `auth.py` โ€“ Authentication System + +Handles secure authentication via: +- Bcrypt hashing (`passlib`) +- JWT creation and validation (`python-jose`) +- Secure route access using FastAPIโ€™s `Depends` + +### Functions + +- `get_password_hash(password)` โ€“ Hash password using bcrypt +- `verify_password(plain, hashed)` โ€“ Verify password +- `create_access_token(data)` โ€“ Return JWT (30-day expiry default) +- `get_current_user()` โ€“ Decodes token and returns `User` model +- `authenticate_user(email, password)` โ€“ Checks login credentials +- `create_user_response(user)` โ€“ Returns `UserResponse` for frontend + +--- + +## `bootstrap.py` โ€“ System Bootstrap + +Runs once on app startup to: +- Determine the default LLM provider (Gemini or Ollama) +- Initialize `ImprovedChatOrchestrator` +- Inject personas using `get_default_personas(llm)` + +```python +llm = create_llm_client() # Gemini or Ollama +chat_orchestrator = ImprovedChatOrchestrator() +DEFAULT_PERSONAS = get_default_personas(llm) +``` + +Each persona is **registered** into the orchestrator using `.register_persona()`. + +--- + +## `context.py` โ€“ Global Per-Session Context + +A basic context storage class (`GlobalSessionContext`) that keeps: +- `full_log`: List of all messages +- `uploaded_files`: Tracked files per session +- `total_upload_size`: Helps enforce limits + +Used primarily in earlier versions or smaller contexts. + +--- + +## `context_manager.py` โ€“ LLM Context Window Formatter + +This class builds optimized context windows for both Gemini and Ollama: + +### `ContextManager.prepare_context_for_llm()` +Returns a `ContextWindow(messages, token_count, truncated)` with: +- LLM-specific formatting +- Automatic message pruning based on token limits +- Recency- and relevance-weighted scoring for old messages +- Automatic stop tokens, system prompts, and formatting + +### Key Features + +| Feature | Gemini | Ollama | +|--------|--------|--------| +| Format | JSON roles + parts | Flat prompt string | +| Role Mapping | 'user', 'model' | 'User:', 'Assistant:' | +| Chunking Strategy | Full doc as `Context Document:` | Plain text injection | +| Stop Sequences | Customizable | Enforced via `stop[]` | + +Used **by all LLM clients** (Ollama/Gemini) and the **orchestrator**. + +--- + +## `database.py` โ€“ MongoDB Connector + +- Uses `motor` for async MongoDB +- Exposes `get_database()` to other modules +- Automatically creates indexes on `users` and `chat_sessions` +- Controlled via `.env` (`MONGODB_CONNECTION_STRING`) + +```python +await connect_to_mongo() +await close_mongo_connection() +``` + +--- + +## `improved_orchestrator.py` โ€“ Brain of the Chatbot + +This is the main **message routing engine**. + +### Main Responsibilities + +- Route user input through: + - Clarification detection + - Document-aware context building + - Persona-level response generation +- Aggregate responses from **multiple advisors** +- Embed document-based context (RAG) + +### Key Functions + +- `process_message()` โ€“ Entry point for chat flow (calls all advisors) +- `chat_with_persona()` โ€“ Talk to one specific advisor +- `_generate_persona_responses()` โ€“ Routes through each registered persona +- `_build_enhanced_context_for_persona()` โ€“ Combines conversation + document info + +### Extras +- Document parsing hints (`"my thesis"`, `"section 2"`, etc.) +- Top-K persona ranking (`get_top_personas()`) +- Persona-specific fallback logic +- Session reset/deletion + +Used by `/chat-stream`, `/reply-to-advisor`, etc. + +--- + +## `rag_manager.py` โ€“ RAG System for Docs + +Supports **vector-based retrieval** using: + +- Sentence Transformers (`all-MiniLM-L6-v2`) +- ChromaDB (`PersistentClient` with metadata) +- Metadata-aware enhanced chunking +- Overlapping token window strategy +- Section-wise classification + +### Core Components + +| Class | Role | +|-------|------| +| `RAGManager` | Standard chunking, basic RAG | +| `EnhancedRAGManager` | Persona-aware + metadata-annotated vector chunks | + +### `EnhancedRAGManager` supports: +- Section tagging (`methodology`, `theory`, etc.) +- Multi-level filters (`session_id`, `filename`) +- Attribution fields (`chunk_position`, `has_methodology`) +- Relevance scoring and ranking + +Used by orchestrator when generating document-aware responses. + +--- + +## `session_manager.py` โ€“ Chat Lifecycle Controller + +Handles: +- In-memory session creation + cleanup (with expiration) +- Tracks uploaded files and size +- Holds message logs for each session +- Links to RAG via `add_uploaded_file()` and `get_rag_stats()` + +### `ConversationContext` + +| Attribute | Description | +|-----------|-------------| +| `messages` | List of role-message entries | +| `uploaded_files` | Filenames (content stored in RAG DB) | +| `document_chunks_count` | Count of indexed doc chunks | +| `last_retrieval_stats` | From last RAG search | +| `created_at`, `last_accessed` | Session activity tracking | + +Includes: +- Reset functions (`clear_all_data()`) +- File-level message logging (`append_message()`) + +### `SessionManager` + +- Thread-safe via locks +- Handles cleanup of expired sessions (`_cleanup_expired_sessions()`) +- Returns statistics via `get_session_stats()` + +--- + +## Interactions Summary + +```text +User Input โ†’ Orchestrator + โ†ณ SessionManager โ†’ Context + โ†ณ RAGManager โ†’ Relevant Docs + โ†ณ LLMClient (Gemini/Ollama) โ† ContextManager +``` \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/core/auth.py b/multi_llm_chatbot_backend/app/core/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..f18a5100e21fbc3f8a9d5a408da104f2e3d651dc --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/auth.py @@ -0,0 +1,119 @@ +import os +from datetime import datetime, timedelta +from typing import Optional +from fastapi import HTTPException, Depends, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from passlib.context import CryptContext +from jose import JWTError, jwt +from bson import ObjectId +from app.core.database import get_database +from app.models.user import User, UserResponse +from app.config import get_settings + +# Password hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# JWT settings โ€” driven by config.yaml with env-var fallback +_cfg = get_settings().auth +SECRET_KEY = _cfg.jwt_secret +ALGORITHM = _cfg.algorithm +ACCESS_TOKEN_EXPIRE_MINUTES = _cfg.token_expiry_minutes + +# Security scheme +security = HTTPBearer() + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash""" + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password: str) -> str: + """Hash a password""" + return pwd_context.hash(password) + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + """Create a JWT access token""" + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + + # JWT NumericDate must be a Unix timestamp (RFC 7519). + to_encode.update({"exp": int(expire.timestamp())}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +async def get_user_by_email(email: str) -> Optional[User]: + """Get user by email from database""" + db = get_database() + user_data = await db.users.find_one({"email": email}) + if user_data: + return User(**user_data) + return None + +async def get_user_by_id(user_id: str) -> Optional[User]: + """Get user by ID from database""" + try: + db = get_database() + user_data = await db.users.find_one({"_id": ObjectId(user_id)}) + if user_data: + return User(**user_data) + return None + except Exception: + return None + +async def authenticate_user(email: str, password: str) -> Optional[User]: + """Authenticate user with email and password""" + user = await get_user_by_email(email) + if not user: + return None + if not verify_password(password, user.hashed_password): + return None + return user + +async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> User: + """Get current user from JWT token""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM]) + user_id: str = payload.get("sub") + if user_id is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + user = await get_user_by_id(user_id) + if user is None: + raise credentials_exception + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Inactive user" + ) + return user + +async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: + """Get current active user""" + if not current_user.is_active: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + +def create_user_response(user: User) -> UserResponse: + """Create user response model""" + return UserResponse( + id=str(user.id), + firstName=user.firstName, + lastName=user.lastName, + email=user.email, + academicStage=user.academicStage, + researchArea=user.researchArea, + avatarId=user.avatarId, + created_at=user.created_at, + last_login=user.last_login + ) diff --git a/multi_llm_chatbot_backend/app/core/bootstrap.py b/multi_llm_chatbot_backend/app/core/bootstrap.py new file mode 100644 index 0000000000000000000000000000000000000000..a6853f7c97a5644fb15e886662c42277f8815144 --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/bootstrap.py @@ -0,0 +1,159 @@ +# app/core/bootstrap.py +import os +from pathlib import Path + +from app.core.env_loader import load_application_env + +load_application_env() + +from app.config import get_settings +from app.llm.improved_gemini_client import ImprovedGeminiClient +from app.llm.improved_ollama_client import ImprovedOllamaClient +from app.llm.improved_vllm_client import ImprovedVllmClient +from app.llm.openai_fallback_client import OpenAIFallbackClient +from app.llm.resilient_client import ResilientLLMClient +from app.core.improved_orchestrator import ImprovedChatOrchestrator +from app.models.default_personas import get_default_personas + +settings = get_settings() + +current_provider = settings.llm.provider or "vllm" +available_providers = ["ollama", "gemini", "vllm", "openai"] + + +def _load_shared_env_var(name: str) -> str: + explicit = os.environ.get("SHARED_ENV", "").strip() + candidates = [Path(explicit)] if explicit else [] + candidates.append(Path.home() / ".secrets" / "shared.env") + prefix = f"{name}=" + for shared in candidates: + if not shared.is_file(): + continue + for line in shared.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if line.startswith(prefix): + return line.split("=", 1)[1].strip() + return "" + + +def _vllm_api_username() -> str: + """Optional HTTP Basic auth username for the vLLM endpoint. + + The Neon BrainForge/Security endpoint at 4090-x1-3 requires HTTP Basic + auth using HANA_USERNAME_KLATCHAT / HANA_KLATCHAT_PASSWORD from + ~/.secrets/shared.env. We allow explicit override via api_username + in config or VLLM_API_USERNAME env var, falling back to the shared + HANA_USERNAME_KLATCHAT entry. + """ + return ( + settings.llm.vllm.api_username + or os.getenv("VLLM_API_USERNAME", "") + or _load_shared_env_var("HANA_USERNAME_KLATCHAT") + ) + + +def _vllm_api_key() -> str: + """vLLM key/password. If api_username is set (HANA Basic auth), prefer + HANA_KLATCHAT_PASSWORD; otherwise use the generic VLLM_API_KEY Bearer + token. This matches the dual-auth nature of the Neon endpoints. + """ + if _vllm_api_username(): + return ( + settings.llm.vllm.api_key + or os.getenv("HANA_KLATCHAT_PASSWORD", "") + or _load_shared_env_var("HANA_KLATCHAT_PASSWORD") + or os.getenv("VLLM_API_KEY", "") + or _load_shared_env_var("VLLM_API_KEY") + ) + return ( + settings.llm.vllm.api_key + or os.getenv("VLLM_API_KEY", "") + or _load_shared_env_var("VLLM_API_KEY") + ) + + +def _openai_api_key() -> str: + return ( + settings.llm.openai.api_key + or os.getenv("OPENAI_API_KEY", "") + or _load_shared_env_var("OPENAI_API_KEY") + ) + + +def _build_neon_vllm(neon_persona: str | None) -> ImprovedVllmClient: + vllm = settings.llm.vllm + if not vllm.api_url: + raise ValueError("No vLLM endpoint configured. Set llm.vllm.api_url in your config.") + model_name = vllm.model_id or None + return ImprovedVllmClient( + api_url=vllm.api_url, + api_key=_vllm_api_key(), + model_name=model_name, + neon_persona=neon_persona, + api_username=_vllm_api_username() or None, + ) + + +def _build_openai(reasoning_effort: str) -> OpenAIFallbackClient: + return OpenAIFallbackClient( + api_key=_openai_api_key(), + model=settings.llm.openai.model, + reasoning_effort=reasoning_effort, + ) + + +def _wrap_resilient(primary: ImprovedVllmClient, fallback: OpenAIFallbackClient, label: str): + return ResilientLLMClient( + primary=primary, + fallback=fallback, + race_timeout_seconds=settings.llm.resilient.race_timeout_seconds, + primary_label=label, + ) + + +def create_llm_client(provider=None): + if provider is None: + provider = current_provider + if provider == "openai": + # Standalone OpenAI (GPT) client. OpenAIFallbackClient implements the same + # LLMClient interface as the Gemini client, so it works as the primary + # orchestrator/persona LLM, not just a vLLM fallback. + return _build_openai(settings.llm.openai.orchestrator_reasoning_effort) + if provider == "gemini": + return ImprovedGeminiClient(model_name=settings.llm.gemini.model) + if provider == "vllm": + neon = settings.llm.vllm.neon_persona_orchestrator + primary = _build_neon_vllm(neon if neon != "vanilla" else None) + fallback = _build_openai(settings.llm.openai.orchestrator_reasoning_effort) + return _wrap_resilient(primary, fallback, "orchestrator") + return ImprovedOllamaClient( + model_name=settings.llm.ollama.model, + base_url=settings.llm.ollama.base_url, + ) + + +def create_orchestrator_llm(): + if current_provider != "vllm": + return create_llm_client() + neon = settings.llm.vllm.neon_persona_orchestrator + primary = _build_neon_vllm(neon if neon != "vanilla" else None) + fallback = _build_openai(settings.llm.openai.orchestrator_reasoning_effort) + return _wrap_resilient(primary, fallback, "orchestrator") + + +def create_persona_llm(): + if current_provider != "vllm": + return create_llm_client() + neon = settings.llm.vllm.neon_persona_advisors + primary = _build_neon_vllm(neon) + fallback = _build_openai(settings.llm.openai.persona_reasoning_effort) + return _wrap_resilient(primary, fallback, "persona") + + +llm = create_orchestrator_llm() +chat_orchestrator = ImprovedChatOrchestrator(llm_client=llm) + +persona_llm = create_persona_llm() if current_provider == "vllm" else llm +DEFAULT_PERSONAS = get_default_personas(persona_llm) +for persona in DEFAULT_PERSONAS: + chat_orchestrator.register_persona(persona) diff --git a/multi_llm_chatbot_backend/app/core/canvas_analysis.py b/multi_llm_chatbot_backend/app/core/canvas_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..ba374dcb8afd7481a1e962b714bdd06cd7f3be66 --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/canvas_analysis.py @@ -0,0 +1,287 @@ +import re +import json +import logging +from typing import Dict, List +from collections import defaultdict + +from app.models.phd_canvas import CanvasInsight +from app.config import get_settings + +logger = logging.getLogger(__name__) + +class CanvasAnalysisService: + """Service for extracting and categorizing insights from chat messages""" + + def __init__(self, llm_client=None): + self.llm_client = llm_client + + # Predefined section mappings for semantic analysis + self.section_keywords = { + "research_progress": [ + "progress", "milestone", "completed", "finished", "accomplished", + "achieved", "timeline", "deadline", "chapter", "draft", "version" + ], + "methodology": [ + "method", "methodology", "approach", "design", "data collection", + "survey", "interview", "experiment", "analysis", "statistical", + "qualitative", "quantitative", "mixed methods" + ], + "theoretical_framework": [ + "theory", "theoretical", "framework", "concept", "model", + "literature", "author", "philosophy", "paradigm", "assumption" + ], + "challenges_obstacles": [ + "challenge", "problem", "difficulty", "obstacle", "stuck", + "confused", "frustrated", "struggle", "barrier", "issue" + ], + "next_steps": [ + "next", "plan", "should", "will", "going to", "need to", + "action", "step", "priority", "focus", "goal", "objective" + ], + "writing_communication": [ + "write", "writing", "paper", "thesis", "dissertation", "publication", + "communication", "presentation", "defense", "draft", "revision" + ], + "career_development": [ + "career", "job", "position", "networking", "conference", + "skills", "CV", "resume", "application", "fellowship", "grant" + ], + "literature_review": [ + "literature", "sources", "papers", "articles", "bibliography", + "citation", "author", "study", "research", "gap", "review" + ], + "data_analysis": [ + "data", "analysis", "results", "findings", "statistics", + "coding", "software", "tool", "visualization", "pattern" + ], + "motivation_mindset": [ + "motivation", "confidence", "stress", "anxiety", "balance", + "mental health", "mindset", "support", "overwhelmed", "burnout" + ] + } + + async def extract_insights_from_messages(self, messages: List[Dict], chat_session_id: str) -> List[CanvasInsight]: + """Extract actionable insights from chat messages using semantic analysis""" + try: + insights = [] + + for message in messages: + # Handle the actual stored message format from your database + msg_type = message.get("type", "") + + if msg_type == "advisor": + # This is the format used in your database + content = message.get("content", "") + persona_id = message.get("advisorName", message.get("persona", "advisor")) + message_id = message.get("id", str(message.get("_id", ""))) + + if content and len(content.strip()) > 20: # Only process substantial content + persona_insights = await self._extract_insights_from_content( + content, persona_id, message_id, chat_session_id + ) + insights.extend(persona_insights) + logger.debug(f"Extracted {len(persona_insights)} insights from {persona_id} message") + + elif msg_type == "assistant" and "responses" in message: + # Handle multi-persona responses (if this format exists) + for response in message.get("responses", []): + persona_insights = await self._extract_insights_from_persona_response( + response, message.get("id"), chat_session_id + ) + insights.extend(persona_insights) + + elif message.get("role") == "assistant" and message.get("content"): + # Handle converted message format (from export functions) + persona_insights = await self._extract_insights_from_content( + message.get("content", ""), "assistant", + message.get("id"), chat_session_id + ) + insights.extend(persona_insights) + + # Remove duplicates and low-confidence insights + unique_insights = self._deduplicate_insights(insights) + high_confidence_insights = [i for i in unique_insights if i.confidence_score >= 0.5] # Lowered threshold + + logger.info(f"Extracted {len(high_confidence_insights)} insights from {len(messages)} messages") + return high_confidence_insights + + except Exception as e: + logger.error(f"Error extracting insights from messages: {e}") + import traceback + logger.error(f"Full traceback: {traceback.format_exc()}") + return [] + + async def _extract_insights_from_persona_response(self, response: Dict, message_id: str, chat_session_id: str) -> List[CanvasInsight]: + """Extract insights from a single persona response""" + persona_id = response.get("persona_id", "unknown") + content = response.get("content", "") + + return await self._extract_insights_from_content(content, persona_id, message_id, chat_session_id) + + async def _extract_insights_from_content(self, content: str, persona_id: str, message_id: str, chat_session_id: str) -> List[CanvasInsight]: + """Extract actionable insights from content using LLM analysis""" + if not content or len(content.strip()) < 30: + return [] + + try: + app_title = get_settings().app.title + extraction_prompt = f""" + Extract actionable insights from this {app_title} advisor response that would be valuable for a user's progress summary: + + PERSONA: {persona_id} + CONTENT: {content} + + Return a JSON list of insights. Each insight should be: + - Actionable and specific to the user's progress + - 1-2 sentences long + - Valuable for advisor meetings + - Not generic advice + + Format: [{{"insight": "specific actionable insight here", "keywords": ["keyword1", "keyword2"]}}] + + Return ONLY the JSON array, no other text. + """ + + if self.llm_client: + try: + llm_response = await self.llm_client.generate( + system_prompt=f"You are an expert at extracting actionable guidance from {app_title} advisor responses.", + context=[{"role": "user", "content": extraction_prompt}], + temperature=0.3, + max_tokens=1000, + response_mime_type="application/json" + ) + + insights_data = json.loads(llm_response) + insights = [] + + for item in insights_data: + if isinstance(item, dict) and "insight" in item: + insight = CanvasInsight( + content=item["insight"], + source_persona=persona_id, + source_message_id=message_id, + source_chat_session=chat_session_id, + confidence_score=0.8, # High confidence from LLM extraction + keywords=item.get("keywords", []) + ) + insights.append(insight) + + if insights: + logger.debug(f"LLM extracted {len(insights)} insights from {persona_id}") + return insights + + logger.warning(f"LLM returned no parseable insights for {persona_id}, falling back to rule-based") + + except Exception as llm_error: + logger.warning(f"LLM extraction failed for {persona_id}: {llm_error}") + + # Fallback if no LLM available + return self._extract_insights_rule_based(content, persona_id, message_id, chat_session_id) + + except Exception as e: + logger.error(f"Error extracting insights from {persona_id} content: {e}") + return [] + + def _extract_insights_rule_based(self, content: str, persona_id: str, message_id: str, chat_session_id: str) -> List[CanvasInsight]: + """Fallback rule-based insight extraction""" + insights = [] + + # Extract actionable sentences + sentences = re.split(r'[.!?]+', content) + actionable_patterns = [ + r"(?:you should|consider|try|focus on|prioritize|next step|recommended?|suggest)", + r"(?:action item|goal|objective|plan|strategy)", + r"(?:deadline|timeline|schedule|by \w+)", + r"(?:complete|finish|work on|tackle|address)" + ] + + for sentence in sentences: + sentence = sentence.strip() + if len(sentence) < 15: # Lowered minimum length + continue + + match_count = sum(1 for pattern in actionable_patterns if re.search(pattern, sentence, re.IGNORECASE)) + + if match_count > 0: + keywords = self._extract_keywords_from_sentence(sentence) + confidence = round(0.55 + min(match_count * 0.05, 0.15), 2) + + insight = CanvasInsight( + content=sentence, + source_persona=persona_id, + source_message_id=message_id, + source_chat_session=chat_session_id, + confidence_score=confidence, + keywords=keywords + ) + insights.append(insight) + + logger.debug(f"Rule-based extracted {len(insights)} insights from {persona_id}") + return insights[:3] # Limit to 3 insights per message to avoid noise + + def _extract_keywords_from_sentence(self, sentence: str) -> List[str]: + """Extract relevant keywords from a sentence""" + # Simple keyword extraction + words = re.findall(r'\b\w{4,}\b', sentence.lower()) # Words with 4+ chars + + # Filter out common words + stop_words = { + "should", "could", "would", "your", "research", "work", "study", + "consider", "think", "important", "also", "really", "maybe", + "probably", "definitely", "certainly", "particular", "specific" + } + + keywords = [word for word in words if word not in stop_words] + return keywords[:5] # Limit to 5 keywords + + def categorize_insights(self, insights: List[CanvasInsight]) -> Dict[str, List[CanvasInsight]]: + """Categorize insights into canvas sections using semantic analysis""" + categorized = defaultdict(list) + + for insight in insights: + section = self._determine_section(insight) + categorized[section].append(insight) + + return dict(categorized) + + def _determine_section(self, insight: CanvasInsight) -> str: + """Determine which section an insight belongs to""" + content_lower = insight.content.lower() + keywords_lower = [k.lower() for k in insight.keywords] + all_text = content_lower + " " + " ".join(keywords_lower) + + # Score each section based on keyword matches + section_scores = {} + for section, keywords in self.section_keywords.items(): + score = sum(1 for keyword in keywords if keyword in all_text) + if score > 0: + section_scores[section] = score + + # Return the section with highest score, or default + if section_scores: + return max(section_scores, key=section_scores.get) + else: + return "general_notes" # Default section + + def prioritize_insights(self, insights: List[CanvasInsight]) -> List[CanvasInsight]: + """Sort insights by priority (confidence score and recency)""" + return sorted(insights, key=lambda x: (x.confidence_score, x.extracted_at), reverse=True) + + def _deduplicate_insights(self, insights: List[CanvasInsight]) -> List[CanvasInsight]: + """Remove duplicate or very similar insights""" + if not insights: + return insights + + unique_insights = [] + seen_content = set() + + for insight in insights: + # Simple deduplication based on content similarity + content_key = insight.content.lower().strip()[:100] # First 100 chars + + if content_key not in seen_content: + seen_content.add(content_key) + unique_insights.append(insight) + + return unique_insights diff --git a/multi_llm_chatbot_backend/app/core/canvas_database.py b/multi_llm_chatbot_backend/app/core/canvas_database.py new file mode 100644 index 0000000000000000000000000000000000000000..0b1e8a4612a2cb624c424924ed236e3650baae41 --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/canvas_database.py @@ -0,0 +1,35 @@ +"""PhD Canvas database setup โ€” SQLite-shim aware. + +The original implementation was a Mongo-only module that created compound +indexes and a TTL index on the ``phd_canvases`` collection at startup. With +the SQLite-on-HF-bucket persistence shim, the schema (including the +``user_id`` index) is declared once at connection time inside +``app.core.db``, so this module becomes a compatibility no-op. + +We keep the function signatures so any caller that still imports +``setup_canvas_collections`` or ``cleanup_old_canvas_data`` keeps working. +""" + +import logging +from typing import Any + +logger = logging.getLogger(__name__) + + +async def setup_canvas_collections(db: Any) -> None: + """No-op on the SQLite shim โ€” indexes are created by ``app.core.db``.""" + logger.info( + "PhD Canvas setup: schema and indexes are managed by the SQLite shim; " + "skipping legacy Mongo index creation." + ) + + +async def cleanup_old_canvas_data(db: Any) -> None: + """Maintenance helper โ€” disabled on the SQLite shim. + + The original used a Mongo ``$lookup`` aggregation against the ``users`` + collection, which the in-process aggregation runner doesn't support. If + you need the orphan cleanup, run it via a separate script that does two + passes (``users``, then ``phd_canvases``) and calls ``delete_many``. + """ + logger.info("cleanup_old_canvas_data is a no-op on the SQLite shim.") diff --git a/multi_llm_chatbot_backend/app/core/canvas_manager.py b/multi_llm_chatbot_backend/app/core/canvas_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..43578e7ffc2b73b3a139dec4a5850381cc88a7cc --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/canvas_manager.py @@ -0,0 +1,408 @@ +import logging +from typing import Dict, List, Optional +from datetime import datetime, timedelta +from bson import ObjectId +import asyncio +import hashlib + +from app.models.phd_canvas import PhdCanvas, CanvasInsight, UpdateCanvasRequest +from app.core.canvas_analysis import CanvasAnalysisService +from app.core.bootstrap import llm + +logger = logging.getLogger(__name__) + +class CanvasManager: + """Manages PhD Canvas creation, updates, and incremental processing""" + + def __init__(self): + self.analysis_service = CanvasAnalysisService(llm_client=llm) + self._db = None + # Add lock dictionary to prevent concurrent updates for the same user + self._update_locks = {} + + def get_database(self): + """Lazy database connection to avoid circular imports""" + if self._db is None: + # Import here to avoid circular import + from app.core.database import get_database + self._db = get_database() + return self._db + + async def get_or_create_canvas(self, user_id: str) -> PhdCanvas: + """Get existing canvas or create new one for user""" + try: + db = self.get_database() + user_object_id = ObjectId(user_id) + + # Try to find existing canvas + canvas_doc = await db.phd_canvases.find_one({"user_id": user_object_id}) + + if canvas_doc: + # Convert to PhdCanvas model + canvas = PhdCanvas(**canvas_doc) + logger.info(f"Found existing canvas for user {user_id} with {canvas.total_insights} insights") + return canvas + else: + # Create new canvas + canvas = PhdCanvas(user_id=user_object_id) + + # Insert into database + result = await db.phd_canvases.insert_one(canvas.dict(by_alias=True)) + canvas.id = result.inserted_id + + logger.info(f"Created new canvas for user {user_id}") + return canvas + + except Exception as e: + logger.error(f"Error getting/creating canvas for user {user_id}: {e}") + # Return empty canvas as fallback + return PhdCanvas(user_id=ObjectId(user_id)) + + async def update_canvas(self, user_id: str, request: UpdateCanvasRequest) -> PhdCanvas: + """Update canvas with latest insights from chat sessions""" + + # Get or create a lock for this user + if user_id not in self._update_locks: + self._update_locks[user_id] = asyncio.Lock() + + # Check if an update is already in progress + if self._update_locks[user_id].locked(): + logger.warning(f"Canvas update already in progress for user {user_id}, skipping duplicate request") + # Return current canvas without updating + return await self.get_or_create_canvas(user_id) + + # Acquire lock to prevent concurrent updates + async with self._update_locks[user_id]: + try: + db = self.get_database() + canvas = await self.get_or_create_canvas(user_id) + + logger.info(f"Updating canvas for user {user_id}, force_full={request.force_full_update}") + + # IMPORTANT: Auto-detect if this should be a full update for first-time canvas + is_first_time_update = ( + canvas.last_chat_processed is None and + canvas.total_insights == 0 and + not request.force_full_update + ) + + if is_first_time_update: + logger.info(f"Auto-detecting first-time canvas update for user {user_id}. Converting to full update.") + request.force_full_update = True + + # Store the timestamp BEFORE we start processing + update_started_at = datetime.utcnow() + + # Determine which chats to process + if request.force_full_update: + # Process all chats + chat_sessions = await self._get_all_user_chat_sessions(user_id) + logger.info(f"Force full update: processing {len(chat_sessions)} total chat sessions") + else: + # Process only chats created/updated after last canvas update + chat_sessions = await self._get_new_chat_sessions(user_id, canvas.last_chat_processed) + logger.info(f"Incremental update: processing {len(chat_sessions)} new chat sessions since {canvas.last_chat_processed}") + + if not chat_sessions: + logger.info("No new chat sessions to process") + return canvas + + # Filter chat sessions if specific ones requested + if request.include_chat_sessions: + chat_sessions = [ + chat for chat in chat_sessions + if str(chat["_id"]) in request.include_chat_sessions + ] + logger.info(f"Filtered to {len(chat_sessions)} specifically requested chat sessions") + + # Track processed chat+message combinations to prevent duplicates + processed_sources = set() + + # Get existing processed sources from canvas + for section in canvas.sections.values(): + for insight in section.insights: + if insight.source_chat_session and insight.source_message_id: + processed_sources.add((insight.source_chat_session, insight.source_message_id)) + + # Process each chat session for insights + all_new_insights = [] + processed_chat_ids = [] + + for chat_session in chat_sessions: + try: + chat_id = str(chat_session["_id"]) + messages = chat_session.get("messages", []) + + if not messages: + continue + + # Check if we've already processed these messages + messages_to_process = [] + for msg in messages: + msg_id = msg.get('id', '') + if (chat_id, msg_id) not in processed_sources: + messages_to_process.append(msg) + + if not messages_to_process: + logger.info(f"All messages from chat {chat_id} already processed, skipping") + continue + + logger.info(f"Processing {len(messages_to_process)} new messages from chat {chat_id}") + + # Extract insights from new messages only + session_insights = await self.analysis_service.extract_insights_from_messages( + messages_to_process, chat_id + ) + + if session_insights: + all_new_insights.extend(session_insights) + processed_chat_ids.append(chat_id) + logger.info(f"Extracted {len(session_insights)} insights from chat {chat_id}") + + except Exception as e: + logger.error(f"Error processing chat session {chat_session.get('_id')}: {e}") + continue + + if all_new_insights: + # Categorize insights by section + categorized_insights = self.analysis_service.categorize_insights(all_new_insights) + + # Update canvas sections + sections_updated = 0 + for section_key, insights in categorized_insights.items(): + if request.exclude_sections and section_key in request.exclude_sections: + continue + + # Prioritize insights before adding + prioritized_insights = self.analysis_service.prioritize_insights(insights) + + # Limit insights per section to avoid overwhelming canvas + max_insights_per_section = 10 + limited_insights = prioritized_insights[:max_insights_per_section] + + if limited_insights: + canvas.update_section(section_key, limited_insights) + sections_updated += 1 + logger.info(f"Updated section '{section_key}' with {len(limited_insights)} insights") + + # Update canvas metadata with the timestamp from BEFORE processing + canvas.last_chat_processed = update_started_at + canvas.last_updated = datetime.utcnow() + + # Save updated canvas to database + await self._save_canvas(canvas) + + logger.info(f"Canvas update completed: {len(all_new_insights)} new insights, {sections_updated} sections updated") + else: + logger.info("No insights extracted from chat sessions") + + return canvas + + except Exception as e: + logger.error(f"Error updating canvas for user {user_id}: {e}") + import traceback + logger.error(f"Full traceback: {traceback.format_exc()}") + raise + finally: + # Clean up lock if no longer needed + if user_id in self._update_locks and not self._update_locks[user_id].locked(): + # Remove lock after some time to prevent memory buildup + pass # Keep lock for potential future use + + async def _get_all_user_chat_sessions(self, user_id: str) -> List[Dict]: + """Get all chat sessions for a user""" + try: + db = self.get_database() + user_object_id = ObjectId(user_id) + + cursor = db.chat_sessions.find({ + "user_id": user_object_id, + "is_active": {"$ne": False}, # Include active sessions + "deleted_at": {"$exists": False} # Exclude deleted sessions + }).sort("created_at", -1) # Most recent first + + chat_sessions = await cursor.to_list(length=100) # Limit to last 100 chats + return chat_sessions + + except Exception as e: + logger.error(f"Error getting all chat sessions for user {user_id}: {e}") + return [] + + async def _get_new_chat_sessions(self, user_id: str, since: Optional[datetime]) -> List[Dict]: + """Get chat sessions created or updated after a specific time""" + try: + db = self.get_database() + user_object_id = ObjectId(user_id) + + # Build query filter + query_filter = { + "user_id": user_object_id, + "is_active": {"$ne": False}, + "deleted_at": {"$exists": False} + } + + if since: + # Get chats created or updated after the since timestamp + query_filter["$or"] = [ + {"created_at": {"$gt": since}}, + {"updated_at": {"$gt": since}} + ] + else: + # IMPORTANT FIX: If no since time and this is being called for incremental update, + # we should still get more chats for proper first-time processing + # The caller should handle the force_full_update logic properly + + # For now, get chats from last 30 days instead of 7 as a safer fallback + # But ideally, first-time canvas should always use force_full_update=True + one_month_ago = datetime.utcnow() - timedelta(days=30) + query_filter["created_at"] = {"$gte": one_month_ago} + + logger.warning(f"Getting new chat sessions for user {user_id} with no 'since' timestamp. " + f"This should only happen for incremental updates. Consider using force_full_update=True for first-time canvas.") + + # Increase limit for incremental updates to catch more historical chats + limit = 100 # Increased from 50 + cursor = db.chat_sessions.find(query_filter).sort("created_at", -1) + chat_sessions = await cursor.to_list(length=limit) + + return chat_sessions + + except Exception as e: + logger.error(f"Error getting new chat sessions for user {user_id}: {e}") + return [] + + async def _save_canvas(self, canvas: PhdCanvas): + """Save canvas to database""" + try: + db = self.get_database() + + # Update existing canvas + await db.phd_canvases.replace_one( + {"_id": canvas.id}, + canvas.dict(by_alias=True), + upsert=True + ) + + logger.info(f"Saved canvas {canvas.id} to database") + + except Exception as e: + logger.error(f"Error saving canvas to database: {e}") + raise + + async def delete_canvas(self, user_id: str) -> bool: + """Delete canvas for a user""" + try: + db = self.get_database() + user_object_id = ObjectId(user_id) + + result = await db.phd_canvases.delete_one({"user_id": user_object_id}) + + if result.deleted_count > 0: + logger.info(f"Deleted canvas for user {user_id}") + return True + else: + logger.info(f"No canvas found to delete for user {user_id}") + return False + + except Exception as e: + logger.error(f"Error deleting canvas for user {user_id}: {e}") + return False + + async def get_canvas_stats(self, user_id: str) -> Dict: + """Get statistics about the user's PhD Canvas""" + try: + canvas = await self.get_or_create_canvas(user_id) + + # Calculate section breakdown + sections_breakdown = {} + for section_key, section in canvas.sections.items(): + sections_breakdown[section_key] = { + "title": section.title, + "insight_count": len(section.insights), + "priority": section.priority, + "last_updated": section.updated_at + } + + return { + "total_insights": canvas.total_insights, + "total_sections": len(canvas.sections), + "last_updated": canvas.last_updated, + "last_chat_processed": canvas.last_chat_processed, + "created_at": canvas.created_at, + "auto_update": canvas.auto_update, + "sections_breakdown": sections_breakdown + } + + except Exception as e: + logger.error(f"Error getting canvas stats for user {user_id}: {e}") + return { + "total_insights": 0, + "total_sections": 0, + "sections_breakdown": {} + } + + async def export_canvas_for_printing(self, user_id: str) -> Dict: + """Export canvas in a format optimized for printing""" + try: + canvas = await self.get_or_create_canvas(user_id) + + # Format sections for printing + sections = [] + for section_key, section in canvas.sections.items(): + formatted_section = { + "title": section.title, + "description": section.description, + "insights": [ + { + "content": insight.content, + "source": insight.source_persona, + "confidence": insight.confidence_score + } + for insight in section.insights[:5] # Limit to top 5 for printing + ] + } + sections.append(formatted_section) + + return { + "user_id": str(canvas.user_id), + "generated_at": datetime.utcnow(), + "total_insights": canvas.total_insights, + "last_updated": canvas.last_updated, + "sections": sections, + "metadata": { + "created_at": canvas.created_at, + "last_chat_processed": canvas.last_chat_processed, + "print_optimized": True + } + } + + except Exception as e: + logger.error(f"Error exporting canvas for printing for user {user_id}: {e}") + raise + + async def toggle_auto_update(self, user_id: str, enabled: bool) -> bool: + """Toggle auto-update setting for a canvas""" + try: + db = self.get_database() + user_object_id = ObjectId(user_id) + + result = await db.phd_canvases.update_one( + {"user_id": user_object_id}, + {"$set": {"auto_update": enabled}} + ) + + return result.modified_count > 0 + + except Exception as e: + logger.error(f"Error toggling auto-update for user {user_id}: {e}") + return False + +# Singleton instance +_canvas_manager_instance = None + +def get_canvas_manager() -> CanvasManager: + """Get singleton instance of CanvasManager""" + global _canvas_manager_instance + if _canvas_manager_instance is None: + _canvas_manager_instance = CanvasManager() + return _canvas_manager_instance \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/core/context.py b/multi_llm_chatbot_backend/app/core/context.py new file mode 100644 index 0000000000000000000000000000000000000000..28185dd2f5d12b7c9e86bbf6a838b7a360d6181e --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/context.py @@ -0,0 +1,17 @@ +# Global context storage +class GlobalSessionContext: + def __init__(self): + self.full_log: list[dict] = [] + self.uploaded_files: list[str] = [] + self.total_upload_size: int = 0 + + def append(self, role: str, content: str): + self.full_log.append({"role": role, "content": content}) + + def filter_by_persona(self, persona_id: str): + return self.full_log + + def clear(self): + self.full_log = [] + self.uploaded_files = [] + self.total_upload_size = 0 \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/core/context_manager.py b/multi_llm_chatbot_backend/app/core/context_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..64416b9257900f9ced5b600a7980304b261c4a49 --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/context_manager.py @@ -0,0 +1,296 @@ +from typing import List, Dict, Optional, Tuple +from dataclasses import dataclass +import re +from datetime import datetime + +@dataclass +class ContextWindow: + """Represents a context window for LLM processing""" + messages: List[dict] + total_tokens: int + truncated: bool = False + +class ContextManager: + """Unified context management for consistent LLM behavior""" + + def __init__(self, + max_context_tokens: int = 8000, + preserve_recent_messages: int = 5, + chars_per_token: float = 4.0): + self.max_context_tokens = max_context_tokens + self.preserve_recent_messages = preserve_recent_messages + self.chars_per_token = chars_per_token + + def prepare_context_for_llm(self, + messages: List[dict], + system_prompt: str, + llm_provider: str = "gemini") -> ContextWindow: + """ + Prepare context for LLM with intelligent windowing and formatting + """ + # Calculate token budget + system_tokens = self._estimate_tokens(system_prompt) + available_tokens = self.max_context_tokens - system_tokens - 500 # Reserve for response + + # Get relevant context window + context_messages = self._get_optimal_context_window(messages, available_tokens) + + # Format for specific LLM provider + formatted_messages = self._format_for_provider(context_messages, system_prompt, llm_provider) + + return ContextWindow( + messages=formatted_messages, + total_tokens=self._estimate_tokens_for_messages(formatted_messages), + truncated=len(context_messages) < len(messages) + ) + + def _get_optimal_context_window(self, messages: List[dict], token_budget: int) -> List[dict]: + """ + Select optimal messages for context window using recency + relevance + """ + if not messages: + return [] + + # Always preserve the most recent messages + recent_messages = messages[-self.preserve_recent_messages:] + recent_tokens = self._estimate_tokens_for_messages(recent_messages) + + if recent_tokens >= token_budget: + # If recent messages exceed budget, truncate to fit + return self._truncate_to_fit(recent_messages, token_budget) + + # Add older messages if we have token budget remaining + remaining_budget = token_budget - recent_tokens + older_messages = messages[:-self.preserve_recent_messages] if len(messages) > self.preserve_recent_messages else [] + + # Score older messages by relevance and recency + scored_messages = self._score_messages_for_relevance(older_messages, messages[-1]['content'] if messages else "") + + # Add highest scoring messages that fit in budget + selected_older = [] + for message, score in scored_messages: + message_tokens = self._estimate_tokens(message['content']) + if message_tokens <= remaining_budget: + selected_older.append(message) + remaining_budget -= message_tokens + else: + break + + # Combine in chronological order + return selected_older + recent_messages + + def _score_messages_for_relevance(self, messages: List[dict], current_query: str) -> List[Tuple[dict, float]]: + """ + Score messages by relevance to current query and recency + """ + scored = [] + current_query_lower = current_query.lower() + + for i, message in enumerate(messages): + score = 0.0 + content_lower = message['content'].lower() + + # Recency score (more recent = higher score) + recency_score = (i + 1) / len(messages) * 0.3 + + # Keyword overlap score + current_words = set(current_query_lower.split()) + message_words = set(content_lower.split()) + overlap = len(current_words.intersection(message_words)) + keyword_score = min(overlap / max(len(current_words), 1) * 0.4, 0.4) + + # Role importance (user questions and document content more important) + role_score = 0.3 if message['role'] in ['user', 'document'] else 0.1 + + score = recency_score + keyword_score + role_score + scored.append((message, score)) + + # Sort by score descending + return sorted(scored, key=lambda x: x[1], reverse=True) + + def _truncate_to_fit(self, messages: List[dict], token_budget: int) -> List[dict]: + """ + Truncate messages to fit within token budget, preserving most recent + """ + result = [] + current_tokens = 0 + + # Add messages from most recent backward + for message in reversed(messages): + message_tokens = self._estimate_tokens(message['content']) + if current_tokens + message_tokens <= token_budget: + result.insert(0, message) + current_tokens += message_tokens + else: + break + + return result + + def _format_for_provider(self, messages: List[dict], system_prompt: str, provider: str) -> List[dict]: + """ + Format messages for specific LLM provider + """ + if provider.lower() == "gemini": + return self._format_for_gemini(messages, system_prompt) + elif provider.lower() in ["ollama", "mistral"]: + return self._format_for_ollama(messages, system_prompt) + elif provider.lower() == "vllm": + return self._format_for_vllm(messages, system_prompt) + else: + # Default format + return [{"role": "system", "content": system_prompt}] + messages + + def _format_for_vllm(self, messages: List[dict], system_prompt: str) -> List[dict]: + """ + Format messages for vLLM's OpenAI-compatible API. + Normalizes custom persona roles to 'assistant' since the API + only accepts system/user/assistant. + """ + formatted = [{"role": "system", "content": system_prompt}] + for message in messages: + role = message["role"] + content = message["content"] + if role == "system": + continue + if role not in ("user", "assistant"): + content = f"[{role.title()} Advisor]: {content}" + role = "assistant" + formatted.append({"role": role, "content": content}) + return formatted + + def _format_for_gemini(self, messages: List[dict], system_prompt: str) -> List[dict]: + """ + Format messages for Gemini API (uses user/model roles with parts structure) + FIXED VERSION - Better handles system messages and document context + """ + formatted = [] + + # Process all messages, handling system messages properly + system_message_content = "" + conversation_messages = [] + + # Add initial system prompt if provided + if system_prompt: + system_message_content = system_prompt + + for message in messages: + role = message['role'] + content = message['content'] + + if role == 'system': + # Accumulate system messages + if system_message_content: + system_message_content += "\n\n" + content + else: + system_message_content = content + else: + conversation_messages.append(message) + + # Add accumulated system content as initial exchange + if system_message_content: + formatted.extend([ + { + "role": "user", + "parts": [{"text": system_message_content}] + }, + { + "role": "model", + "parts": [{"text": "I understand. I'll follow these instructions and use the document context you've provided."}] + } + ]) + + # Convert conversation messages to Gemini format + for message in conversation_messages: + role = message['role'] + content = message['content'] + + if role == 'user': + formatted.append({ + "role": "user", + "parts": [{"text": content}] + }) + elif role == 'document': + formatted.append({ + "role": "user", + "parts": [{"text": f"[Context Document] {content}"}] + }) + else: + formatted.append({ + "role": "model", + "parts": [{"text": content}] + }) + + return formatted + + def _format_for_ollama(self, messages: List[dict], system_prompt: str) -> str: + """ + Format messages for Ollama (returns formatted prompt string) + """ + parts = [system_prompt] if system_prompt else [] + + for message in messages: + role = message['role'].capitalize() + content = message['content'] + + if role == 'Document': + parts.append(f"Context: {content}") + else: + parts.append(f"{role}: {content}") + + parts.append("Assistant:") + return "\n\n".join(parts) + + def _estimate_tokens(self, text: str) -> int: + """ + Estimate token count for text + """ + return int(len(text) / self.chars_per_token) + + def _estimate_tokens_for_messages(self, messages: List[dict]) -> int: + """ + Estimate total tokens for list of messages + """ + if isinstance(messages, str): + return self._estimate_tokens(messages) + + total = 0 + for message in messages: + if isinstance(message, dict): + if 'content' in message: + total += self._estimate_tokens(message['content']) + elif 'parts' in message: + # Gemini format + for part in message['parts']: + if 'text' in part: + total += self._estimate_tokens(part['text']) + else: + total += self._estimate_tokens(str(message)) + + return total + + def get_context_summary(self, messages: List[dict]) -> Dict[str, any]: + """ + Get summary information about context + """ + if not messages: + return {"total_messages": 0, "estimated_tokens": 0, "roles": {}} + + role_counts = {} + for message in messages: + role = message.get('role', 'unknown') + role_counts[role] = role_counts.get(role, 0) + 1 + + return { + "total_messages": len(messages), + "estimated_tokens": self._estimate_tokens_for_messages(messages), + "roles": role_counts, + "oldest_message": messages[0].get('timestamp', 'unknown') if messages else None, + "newest_message": messages[-1].get('timestamp', 'unknown') if messages else None + } + +# Global context manager instance +context_manager = ContextManager() + +def get_context_manager() -> ContextManager: + """Get the global context manager instance""" + return context_manager \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/core/database.py b/multi_llm_chatbot_backend/app/core/database.py new file mode 100644 index 0000000000000000000000000000000000000000..6f33d67d73699eb4dd24c99ccf80581ea29e9764 --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/database.py @@ -0,0 +1,50 @@ +"""Database connection facade. + +Used to wrap pymongo / motor against a real MongoDB instance. For Hugging +Face Spaces deployment we now back the same public API with SQLite on a HF +Storage Bucket mount (see ``app.core.db``). The shim emulates the Mongo +idioms the routers and services use, so this module simply forwards to it +and keeps the legacy ``connect_to_mongo`` / ``close_mongo_connection`` / +``get_database`` API intact for ``main.py`` and the route files. +""" + +import logging + +from app.core.db import _close, _open, get_database as _get_database + +logger = logging.getLogger(__name__) + + +async def connect_to_mongo() -> None: + """Open the SQLite-backed database and run schema bootstrap.""" + await _open() + logger.info("SQLite-backed database ready (Mongo API shim active).") + # Legacy hook: previously called ``setup_canvas_collections`` here. + # The phd_canvases table is already declared in the shim's schema, so + # the call below is a no-op but kept for symmetry with the original + # startup path. + try: + from app.core.canvas_database import setup_canvas_collections + + await setup_canvas_collections(_get_database()) + logger.info("Canvas database initialization completed (no-op on SQLite shim).") + except Exception as canvas_error: # pragma: no cover + logger.error(f"Canvas database initialization failed: {canvas_error}") + + +async def close_mongo_connection() -> None: + """Best-effort connection close on FastAPI shutdown.""" + await _close() + logger.info("Disconnected from SQLite-backed database.") + + +async def create_indexes() -> None: + """Index creation is handled by the shim's schema bootstrap; kept for + backwards compatibility so any out-of-process maintenance script that + imports this name keeps working.""" + return None + + +def get_database(): + """Return the SQLite-backed Mongo-API-compatible Database object.""" + return _get_database() diff --git a/multi_llm_chatbot_backend/app/core/db.py b/multi_llm_chatbot_backend/app/core/db.py new file mode 100644 index 0000000000000000000000000000000000000000..844db6e16829b193306c2d97083aed7272bf0d1a --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/db.py @@ -0,0 +1,715 @@ +"""Persistent storage facade backed by SQLite + filesystem on a Hugging Face +Storage Bucket mount. + +Background +---------- +This Space used to require MongoDB (via Motor / GridFS). On Hugging Face +Spaces the container has no Mongo, the legacy ``/data`` Persistent Storage +was retired, and the user does not want to use Atlas (or any paid third +party). The replacement is a single Hugging Face Storage Bucket mounted into +the Space at ``$DATA_DIR`` (default ``/data``). Everything durable lives +there: + + ${DATA_DIR}/launchpad_panel.db โ€” SQLite database (users, chat + sessions, profiles, onboarding, + canvases) + +A single ``hf buckets sync hf://buckets// ./backup`` therefore +captures the full state and can be restored before the next redeploy. + +Design +------ +This module exposes the **subset** of the Motor / pymongo API that the +existing routers and services already call (``find_one``, ``find``, +``insert_one``, ``update_one``, ``update_many``, ``delete_one``, +``delete_many``, ``replace_one``, ``count_documents``, +``find_one_and_delete``, ``aggregate`` with ``$match``/``$group``/``$in``). +The on-disk format is plain JSON-encoded documents in SQLite tables (one row +per document, with an indexed ``_id`` primary key, optional ``email`` UNIQUE +column on ``users``, and an indexed/UNIQUE ``user_id`` column on the user +scoped collections). + +Why not full SQL? The router code uses Mongo idioms (``$in``, ``$set``, +``find_one_and_delete``, an aggregation pipeline) and ``bson.ObjectId`` IDs. +A small in-process query engine over JSON dicts keeps the routers untouched +while still benefiting from SQLite's durability and crash safety on the +bucket mount. + +Why SQLite and not pure JSON files? Atomic commit, crash-consistent writes, +unique constraints (email, user-scoped profile/onboarding), and one +mongodump-style salvage file. WAL mode is *attempted* but if the bucket +FUSE doesn't support the required fcntl/mmap semantics SQLite falls back to +the default rollback journal automatically and this module logs a warning. + +Errors from the underlying storage are re-raised as +``pymongo.errors.PyMongoError`` (or ``DuplicateKeyError`` for unique-index +violations) so the existing 5xx / DuplicateKey handlers keep working +unchanged. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import os +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, AsyncIterator, Mapping, Sequence + +import aiosqlite +from bson import ObjectId +from pymongo.errors import DuplicateKeyError, PyMongoError + +LOG = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Schema +# --------------------------------------------------------------------------- + +# Each entry: (table name, has_unique_email_column, user_id_unique). +# users have a UNIQUE email column. user_profiles and onboarding_conversations +# have a UNIQUE user_id column (one record per user). chat_sessions and +# phd_canvases keep user_id only as an index (many records per user). +_TABLES: tuple[tuple[str, bool, bool], ...] = ( + ("users", True, False), + ("chat_sessions", False, False), + ("user_profiles", False, True), + ("onboarding_conversations", False, True), + ("phd_canvases", False, False), +) + +_TABLE_NAMES = {t[0] for t in _TABLES} + + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + +def _data_dir() -> Path: + raw = os.environ.get("DATA_DIR") or "/data" + candidates = [Path(raw).expanduser()] + if str(candidates[0]) == "/data": + candidates.append(Path.home() / "data") + for p in candidates: + try: + p.mkdir(parents=True, exist_ok=True) + return p + except PermissionError: + LOG.warning("Cannot create data dir %s; trying fallback", p) + fallback = Path("/tmp/data") + fallback.mkdir(parents=True, exist_ok=True) + return fallback + + +def _db_path() -> Path: + return _data_dir() / "launchpad_panel.db" + + +# --------------------------------------------------------------------------- +# JSON (de)serialization that round-trips ObjectId / datetime +# --------------------------------------------------------------------------- + +def _enc(o: Any) -> Any: + if isinstance(o, ObjectId): + return {"$oid": str(o)} + if isinstance(o, datetime): + if o.tzinfo is None: + o = o.replace(tzinfo=timezone.utc) + return {"$date": o.isoformat()} + raise TypeError(f"Type {type(o)!r} is not JSON serialisable") + + +def _hook(d: dict) -> Any: + if len(d) == 1: + if "$oid" in d: + return ObjectId(d["$oid"]) + if "$date" in d: + return datetime.fromisoformat(d["$date"]) + return d + + +def _dumps(doc: Mapping) -> str: + return json.dumps(dict(doc), default=_enc) + + +def _loads(text: str) -> dict: + return json.loads(text, object_hook=_hook) + + +def _id_str(v: Any) -> str: + if isinstance(v, ObjectId): + return str(v) + if isinstance(v, str): + return v + return str(v) + + +# --------------------------------------------------------------------------- +# Connection management (single shared aiosqlite connection) +# --------------------------------------------------------------------------- + +_conn: aiosqlite.Connection | None = None +_open_lock = asyncio.Lock() + + +async def _open() -> aiosqlite.Connection: + global _conn + if _conn is not None: + return _conn + async with _open_lock: + if _conn is not None: + return _conn + path = _db_path() + try: + conn = await aiosqlite.connect(str(path)) + except (aiosqlite.OperationalError, OSError) as exc: + raise PyMongoError(f"Cannot open database at {path}: {exc}") from exc + + # WAL gives concurrent readers; if the bucket FUSE doesn't support the + # mmap/lock semantics WAL needs, SQLite stays in the default rollback + # journal which still works on every filesystem we care about. + try: + cur = await conn.execute("PRAGMA journal_mode=WAL") + row = await cur.fetchone() + mode = (row[0] if row else "?") + if str(mode).lower() != "wal": + LOG.warning( + "Could not enable WAL mode on %s (got %r); using rollback journal", + path, mode, + ) + except aiosqlite.OperationalError as exc: + LOG.warning("PRAGMA journal_mode=WAL failed (%s); using rollback journal", exc) + + try: + await conn.execute("PRAGMA synchronous=NORMAL") + await conn.execute("PRAGMA foreign_keys=ON") + except aiosqlite.OperationalError: + pass + + await _init_schema(conn) + await conn.commit() + LOG.info("SQLite ready at %s", path) + _conn = conn + return _conn + + +async def _init_schema(conn: aiosqlite.Connection) -> None: + parts: list[str] = [] + for name, has_email, user_id_unique in _TABLES: + if has_email: + parts.append( + f""" + CREATE TABLE IF NOT EXISTS {name} ( + _id TEXT PRIMARY KEY, + email TEXT UNIQUE, + doc TEXT NOT NULL + ); + """ + ) + else: + uniq = "UNIQUE" if user_id_unique else "" + parts.append( + f""" + CREATE TABLE IF NOT EXISTS {name} ( + _id TEXT PRIMARY KEY, + user_id TEXT {uniq}, + doc TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_{name}_user_id ON {name}(user_id); + """ + ) + await conn.executescript("\n".join(parts)) + + +async def _close() -> None: + """Best-effort connection close (called from FastAPI lifespan teardown).""" + global _conn + if _conn is not None: + try: + await _conn.close() + except Exception: # pragma: no cover - shutdown path + pass + _conn = None + + +# --------------------------------------------------------------------------- +# Filter matching (only the Mongo operators the routers actually use) +# --------------------------------------------------------------------------- + +def _match_value(actual: Any, want: Any) -> bool: + if isinstance(want, dict) and len(want) == 1: + op, arg = next(iter(want.items())) + if op == "$in": + return actual in arg + return actual == want + + +def _matches(doc: Mapping, query: Mapping) -> bool: + for k, v in query.items(): + if not _match_value(doc.get(k), v): + return False + return True + + +# --------------------------------------------------------------------------- +# Result types (shaped like pymongo's so existing ``res.inserted_id`` etc. works) +# --------------------------------------------------------------------------- + +class _InsertOneResult: + def __init__(self, inserted_id: ObjectId) -> None: + self.inserted_id = inserted_id + + +class _UpdateResult: + def __init__(self, matched: int, modified: int, upserted_id: ObjectId | None = None) -> None: + self.matched_count = matched + self.modified_count = modified + self.upserted_id = upserted_id + + +class _DeleteResult: + def __init__(self, deleted: int) -> None: + self.deleted_count = deleted + + +# --------------------------------------------------------------------------- +# Cursor / aggregation +# --------------------------------------------------------------------------- + +def _sort_key_factory(field: str): + def keyfn(d: Mapping): + v = d.get(field) + return (v is None, v) + return keyfn + + +class _Cursor: + """Lazy cursor: docs are fetched the first time you await iteration / to_list.""" + + def __init__(self, fetch): + self._fetch = fetch + self._sort_field: str | None = None + self._sort_dir: int = 1 + self._limit: int | None = None + self._skip: int = 0 + + def sort(self, field: str | list[tuple[str, int]], direction: int = 1) -> "_Cursor": + # Routers also call .sort([("created_at", -1)]) โ€” accept that form too, + # honoring only the first key (Mongo allows compound sort; for the + # routers we have, single-key sort is sufficient). + if isinstance(field, list) and field: + f, d = field[0] + self._sort_field = f + self._sort_dir = d + else: + self._sort_field = field # type: ignore[assignment] + self._sort_dir = direction + return self + + def limit(self, n: int) -> "_Cursor": + self._limit = int(n) + return self + + def skip(self, n: int) -> "_Cursor": + self._skip = int(n) + return self + + async def _materialize(self) -> list[dict]: + items = await self._fetch() + if self._sort_field is not None: + items.sort(key=_sort_key_factory(self._sort_field), reverse=self._sort_dir < 0) + if self._skip: + items = items[self._skip:] + if self._limit is not None: + items = items[: self._limit] + return items + + async def to_list(self, length: int | None = None) -> list[dict]: + items = await self._materialize() + return items if length is None else items[:length] + + def __aiter__(self) -> AsyncIterator[dict]: + async def _gen(): + for d in await self._materialize(): + yield d + return _gen() + + +class _Aggregate: + """Tiny aggregation runner โ€” supports the pipeline shapes the routers use: + ``$match`` and ``$group`` with ``_id: None`` plus ``$sum``. + + More exotic shapes (``$lookup``) raise NotImplementedError; the only + caller is a maintenance ``cleanup_old_canvas_data`` function that isn't + on the request path, so this is safe. + """ + + def __init__(self, fetch, pipeline: Sequence[Mapping]): + self._fetch = fetch + self._pipeline = list(pipeline) + + async def to_list(self, length: int | None = None) -> list[dict]: + items = await self._fetch() + for stage in self._pipeline: + if "$match" in stage: + items = [d for d in items if _matches(d, stage["$match"])] + elif "$group" in stage: + spec = stage["$group"] + gid = spec.get("_id") + if gid is not None: + raise NotImplementedError("Only $group with _id: None is supported") + bucket: dict[str, Any] = {"_id": None} + for k, v in spec.items(): + if k == "_id": + continue + if not (isinstance(v, dict) and len(v) == 1): + raise NotImplementedError(f"Unsupported $group field: {k}={v!r}") + op, arg = next(iter(v.items())) + if op == "$sum" and isinstance(arg, str) and arg.startswith("$"): + field = arg[1:] + bucket[k] = sum(int(d.get(field) or 0) for d in items) + else: + raise NotImplementedError(f"Unsupported $group op: {op}") + items = [bucket] + else: + raise NotImplementedError(f"Unsupported aggregation stage: {stage}") + return items if length is None else items[:length] + + +# --------------------------------------------------------------------------- +# Collection +# --------------------------------------------------------------------------- + +class Collection: + def __init__(self, name: str) -> None: + self._name = name + self._has_email = name == "users" + + # ----- low-level helpers ------------------------------------------------ + + async def _all(self, conn: aiosqlite.Connection) -> list[dict]: + cur = await conn.execute(f"SELECT doc FROM {self._name}") + rows = await cur.fetchall() + return [_loads(r[0]) for r in rows] + + def _insert_params(self, _id: str, email: str | None, user_id: str | None, doc_json: str): + if self._has_email: + return ( + "INSERT INTO users (_id, email, doc) VALUES (?, ?, ?)", + (_id, email, doc_json), + ) + return ( + f"INSERT INTO {self._name} (_id, user_id, doc) VALUES (?, ?, ?)", + (_id, user_id, doc_json), + ) + + async def _update_row( + self, + conn: aiosqlite.Connection, + _id: str, + email: str | None, + user_id: str | None, + doc_json: str, + ) -> None: + if self._has_email: + await conn.execute( + "UPDATE users SET email=?, doc=? WHERE _id=?", + (email, doc_json, _id), + ) + else: + await conn.execute( + f"UPDATE {self._name} SET user_id=?, doc=? WHERE _id=?", + (user_id, doc_json, _id), + ) + + def _apply_update(self, doc: dict, update: Mapping, is_insert: bool = False) -> None: + for op, fields in update.items(): + if op == "$set": + for k, v in fields.items(): + doc[k] = v + elif op == "$unset": + for k in fields: + doc.pop(k, None) + elif op == "$inc": + for k, v in fields.items(): + doc[k] = (doc.get(k) or 0) + v + elif op == "$push": + # Supports {"$push": {"field": value}} and + # {"$push": {"field": {"$each": [v1, v2, ...]}}}. + for k, spec in fields.items(): + existing = doc.get(k) + if not isinstance(existing, list): + existing = [] + doc[k] = existing + if isinstance(spec, Mapping) and "$each" in spec: + items = list(spec["$each"]) + else: + items = [spec] + existing.extend(items) + elif op == "$setOnInsert": + # Only applies on the insert side of an upsert. + if is_insert: + for k, v in fields.items(): + doc.setdefault(k, v) + else: + raise NotImplementedError(f"Unsupported update operator: {op}") + + # ----- public API ------------------------------------------------------- + + async def find_one(self, query: Mapping | None = None, projection: Mapping | None = None) -> dict | None: + query = dict(query or {}) + try: + conn = await _open() + # Fast paths for primary-key / unique-index lookups. + if list(query.keys()) == ["_id"] and isinstance(query["_id"], (ObjectId, str)): + cur = await conn.execute( + f"SELECT doc FROM {self._name} WHERE _id = ?", + (_id_str(query["_id"]),), + ) + row = await cur.fetchone() + return _loads(row[0]) if row else None + if self._has_email and list(query.keys()) == ["email"]: + cur = await conn.execute( + "SELECT doc FROM users WHERE email = ?", + (query["email"],), + ) + row = await cur.fetchone() + return _loads(row[0]) if row else None + for d in await self._all(conn): + if _matches(d, query): + return d + return None + except (aiosqlite.OperationalError, aiosqlite.DatabaseError, OSError) as exc: + raise PyMongoError(f"find_one({self._name}) failed: {exc}") from exc + + def find(self, query: Mapping | None = None, projection: Mapping | None = None) -> _Cursor: + q = dict(query or {}) + + async def _go() -> list[dict]: + try: + conn = await _open() + docs = await self._all(conn) + return [d for d in docs if _matches(d, q)] + except (aiosqlite.OperationalError, aiosqlite.DatabaseError, OSError) as exc: + raise PyMongoError(f"find({self._name}) failed: {exc}") from exc + + return _Cursor(_go) + + def aggregate(self, pipeline: Sequence[Mapping]) -> _Aggregate: + async def _go() -> list[dict]: + try: + conn = await _open() + return await self._all(conn) + except (aiosqlite.OperationalError, aiosqlite.DatabaseError, OSError) as exc: + raise PyMongoError(f"aggregate({self._name}) failed: {exc}") from exc + + return _Aggregate(_go, pipeline) + + async def count_documents(self, filter: Mapping | None = None) -> int: + f = dict(filter or {}) + try: + conn = await _open() + if not f: + cur = await conn.execute(f"SELECT COUNT(*) FROM {self._name}") + row = await cur.fetchone() + return int(row[0]) if row else 0 + return sum(1 for d in await self._all(conn) if _matches(d, f)) + except (aiosqlite.OperationalError, aiosqlite.DatabaseError, OSError) as exc: + raise PyMongoError(f"count_documents({self._name}) failed: {exc}") from exc + + async def insert_one(self, doc: Mapping) -> _InsertOneResult: + d = dict(doc) + if "_id" not in d or d["_id"] is None: + d["_id"] = ObjectId() + oid = d["_id"] + try: + conn = await _open() + email = d.get("email") if self._has_email else None + user_id = _id_str(d["user_id"]) if "user_id" in d else None + sql, params = self._insert_params(_id_str(oid), email, user_id, _dumps(d)) + await conn.execute(sql, params) + await conn.commit() + return _InsertOneResult(oid) + except aiosqlite.IntegrityError as exc: + raise DuplicateKeyError(str(exc)) from exc + except (aiosqlite.OperationalError, aiosqlite.DatabaseError, OSError) as exc: + raise PyMongoError(f"insert_one({self._name}) failed: {exc}") from exc + + async def update_one( + self, + filter: Mapping, + update: Mapping, + upsert: bool = False, + ) -> _UpdateResult: + try: + conn = await _open() + doc = await self.find_one(filter) + if doc is None and not upsert: + return _UpdateResult(0, 0) + if doc is None and upsert: + base: dict[str, Any] = {} + for k, v in filter.items(): + if not (isinstance(v, dict) and any(str(kk).startswith("$") for kk in v)): + base[k] = v + if "_id" not in base: + base["_id"] = ObjectId() + self._apply_update(base, update, is_insert=True) + res = await self.insert_one(base) + return _UpdateResult(0, 0, upserted_id=res.inserted_id) + self._apply_update(doc, update) + email = doc.get("email") if self._has_email else None + user_id = _id_str(doc["user_id"]) if "user_id" in doc else None + await self._update_row(conn, _id_str(doc["_id"]), email, user_id, _dumps(doc)) + await conn.commit() + return _UpdateResult(1, 1) + except aiosqlite.IntegrityError as exc: + raise DuplicateKeyError(str(exc)) from exc + except (aiosqlite.OperationalError, aiosqlite.DatabaseError, OSError) as exc: + raise PyMongoError(f"update_one({self._name}) failed: {exc}") from exc + + async def update_many( + self, + filter: Mapping, + update: Mapping, + ) -> _UpdateResult: + try: + conn = await _open() + docs = [d for d in await self._all(conn) if _matches(d, filter)] + if not docs: + return _UpdateResult(0, 0) + for doc in docs: + self._apply_update(doc, update) + email = doc.get("email") if self._has_email else None + user_id = _id_str(doc["user_id"]) if "user_id" in doc else None + await self._update_row(conn, _id_str(doc["_id"]), email, user_id, _dumps(doc)) + await conn.commit() + return _UpdateResult(len(docs), len(docs)) + except aiosqlite.IntegrityError as exc: + raise DuplicateKeyError(str(exc)) from exc + except (aiosqlite.OperationalError, aiosqlite.DatabaseError, OSError) as exc: + raise PyMongoError(f"update_many({self._name}) failed: {exc}") from exc + + async def replace_one( + self, + filter: Mapping, + replacement: Mapping, + upsert: bool = False, + ) -> _UpdateResult: + try: + conn = await _open() + existing = await self.find_one(filter) + new_doc = dict(replacement) + if existing is None and not upsert: + return _UpdateResult(0, 0) + if existing is None and upsert: + if "_id" not in new_doc or new_doc["_id"] is None: + new_doc["_id"] = ObjectId() + res = await self.insert_one(new_doc) + return _UpdateResult(0, 0, upserted_id=res.inserted_id) + # Preserve the existing _id so the replacement updates the same row. + new_doc["_id"] = existing["_id"] + email = new_doc.get("email") if self._has_email else None + user_id = _id_str(new_doc["user_id"]) if "user_id" in new_doc else None + await self._update_row(conn, _id_str(existing["_id"]), email, user_id, _dumps(new_doc)) + await conn.commit() + return _UpdateResult(1, 1) + except aiosqlite.IntegrityError as exc: + raise DuplicateKeyError(str(exc)) from exc + except (aiosqlite.OperationalError, aiosqlite.DatabaseError, OSError) as exc: + raise PyMongoError(f"replace_one({self._name}) failed: {exc}") from exc + + async def delete_one(self, filter: Mapping) -> _DeleteResult: + try: + conn = await _open() + doc = await self.find_one(filter) + if doc is None: + return _DeleteResult(0) + await conn.execute( + f"DELETE FROM {self._name} WHERE _id=?", + (_id_str(doc["_id"]),), + ) + await conn.commit() + return _DeleteResult(1) + except (aiosqlite.OperationalError, aiosqlite.DatabaseError, OSError) as exc: + raise PyMongoError(f"delete_one({self._name}) failed: {exc}") from exc + + async def delete_many(self, filter: Mapping) -> _DeleteResult: + try: + conn = await _open() + docs = [d for d in await self._all(conn) if _matches(d, filter)] + for d in docs: + await conn.execute( + f"DELETE FROM {self._name} WHERE _id=?", + (_id_str(d["_id"]),), + ) + if docs: + await conn.commit() + return _DeleteResult(len(docs)) + except (aiosqlite.OperationalError, aiosqlite.DatabaseError, OSError) as exc: + raise PyMongoError(f"delete_many({self._name}) failed: {exc}") from exc + + async def find_one_and_delete(self, filter: Mapping) -> dict | None: + try: + conn = await _open() + doc = await self.find_one(filter) + if doc is None: + return None + await conn.execute( + f"DELETE FROM {self._name} WHERE _id=?", + (_id_str(doc["_id"]),), + ) + await conn.commit() + return doc + except (aiosqlite.OperationalError, aiosqlite.DatabaseError, OSError) as exc: + raise PyMongoError(f"find_one_and_delete({self._name}) failed: {exc}") from exc + + async def create_index(self, keys: Any, unique: bool = False, **_: Any) -> str: + # The schema already declares the indexes the routers rely on + # (PK on _id, UNIQUE on users.email and on user_profiles.user_id / + # onboarding_conversations.user_id, plain index on user_id elsewhere). + # Stays a no-op so legacy main.py / canvas_database.py calls keep + # working without changes. + return "ok" + + +# --------------------------------------------------------------------------- +# Database +# --------------------------------------------------------------------------- + +class Database: + def __init__(self) -> None: + self._collections: dict[str, Collection] = {} + + def _coll(self, name: str) -> Collection: + c = self._collections.get(name) + if c is None: + c = Collection(name) + self._collections[name] = c + return c + + def __getattr__(self, name: str) -> Collection: + if name.startswith("_"): + raise AttributeError(name) + return self._coll(name) + + def __getitem__(self, name: str) -> Collection: + return self._coll(name) + + +_database: Database | None = None + + +def get_database() -> Database: + global _database + if _database is None: + _database = Database() + return _database + + +__all__ = [ + "Collection", + "Database", + "get_database", +] diff --git a/multi_llm_chatbot_backend/app/core/env_loader.py b/multi_llm_chatbot_backend/app/core/env_loader.py new file mode 100644 index 0000000000000000000000000000000000000000..d0e06e12908ad93225224b5f3a3b55c417cdfe94 --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/env_loader.py @@ -0,0 +1,31 @@ +"""Load local and shared environment files before LLM bootstrap.""" + +from __future__ import annotations + +import os +from pathlib import Path + +from dotenv import load_dotenv + +_BACKEND_ROOT = Path(__file__).resolve().parents[2] + + +def _shared_env_paths() -> list[Path]: + paths: list[Path] = [] + explicit = os.environ.get("SHARED_ENV", "").strip() + if explicit: + paths.append(Path(explicit)) + paths.append(Path.home() / ".secrets" / "shared.env") + return paths + + +def load_application_env() -> None: + """Load cwd .env, backend .env, then shared secrets (without overriding explicit env).""" + load_dotenv() + backend_env = _BACKEND_ROOT / ".env" + if backend_env.is_file(): + load_dotenv(backend_env, override=False) + for path in _shared_env_paths(): + if path.is_file(): + load_dotenv(path, override=False) + return diff --git a/multi_llm_chatbot_backend/app/core/improved_orchestrator.py b/multi_llm_chatbot_backend/app/core/improved_orchestrator.py new file mode 100644 index 0000000000000000000000000000000000000000..0141b28a715eb26ae8fec801b32043abdd9c9f1b --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/improved_orchestrator.py @@ -0,0 +1,1119 @@ +from typing import Dict, List, Optional, Any +from app.models.persona import Persona +from app.core.session_manager import ConversationContext, get_session_manager +from app.core.context_manager import get_context_manager +from app.core.rag_manager import get_rag_manager +from app.config import get_settings +from app.llm.llm_client import LLMClient, ToolCallResult +from app.tools import get_tool_definitions, get_tool_executor +from app.tools.nutrition_calculator import ( + compute_protein_advisory_context, + format_computed_context_block, +) +from app.utils.chat_summary import generate_conversation_context_summary + +import json +import logging +import re + +logger = logging.getLogger(__name__) + +# Short replies that supply data (weight, height, age) in response to a prior question. +_FOLLOW_UP_DATA_PATTERNS = [ + r"^\s*i['']?m\s+\d", + r"^\s*i am\s+\d", + r"\d+\s*(?:lbs?|pounds?|kg|kilos?|kgs)\b", + r"^\s*\d{2,3}\s*(?:lbs?|pounds?|kg|kilos?|kgs)?\s*$", + r"^\s*\d+\s*(?:years?\s*old|yo)\b", + r"^\s*(?:about|around|roughly)\s+\d", +] + +# Prior-turn topics that make a short data reply actionable (e.g. protein + weight). +_FOLLOW_UP_TOPIC_KEYWORDS = [ + "protein", "macro", "calorie", "weight", "body weight", "bulk", "cut", + "recomp", "hypertrophy", "muscle", "nutrition", "meal", "diet", + "how much", "how many", "grams", "g/day", "schedule", "days per week", + "equipment", "experience", "goal", +] + + +class ImprovedChatOrchestrator: + """ + Enhanced orchestrator with document awareness and improved context handling + """ + + def __init__(self, llm_client: LLMClient = None): + self.personas: Dict[str, Persona] = {} + self.llm_client = llm_client + self.session_manager = get_session_manager() + self.context_manager = get_context_manager() + + def register_persona(self, persona: Persona): + """Register a persona with the orchestrator""" + self.personas[persona.id] = persona + logger.info(f"Registered persona: {persona.id} ({persona.name})") + + def get_persona(self, persona_id: str) -> Optional[Persona]: + """Get a specific persona""" + return self.personas.get(persona_id) + + def list_personas(self) -> List[str]: + """List all available persona IDs""" + return list(self.personas.keys()) + + async def get_tool_response(self, user_message: str) -> ToolCallResult: + """Check whether a tool can handle *user_message*. + + If tools are disabled in config, no LLM client is available, or the + model decides no tool is needed, returns + ``ToolCallResult(used_tool=False)``. Otherwise executes the tool and + returns the grounded response with ``used_tool=True``. + """ + if self.llm_client is None: + return ToolCallResult(text="", used_tool=False) + + settings = get_settings() + tools_enabled = settings.tools.get_enabled_names() + + if not tools_enabled: + return ToolCallResult(text="", used_tool=False) + + tool_definitions = get_tool_definitions(enabled=tools_enabled) + tool_executor = get_tool_executor(enabled=tools_enabled) + + if not tool_definitions: + return ToolCallResult(text="", used_tool=False) + + app_cfg = settings.app + system_prompt = ( + f"You are a helpful assistant for {app_cfg.title} โ€” {app_cfg.subtitle}. " + "You have access to external tools. " + "Use the available tools when the user's question can be answered by one of them. " + "Call get_current_datetime when the user asks about today, deadlines, timelines, " + "schedules, or when accurate date/time context would improve " + "your guidance โ€” then weave the result into your answer. " + "Call calculate_protein_target when the user asks about protein intake and " + "provides body weight โ€” use the returned grams, do not guess arithmetic. " + "If no tool is relevant, respond with a brief text answer. " + "Format your responses using markdown." + ) + + return await self.llm_client.generate_with_tools( + system_prompt=system_prompt, + user_message=user_message, + tool_definitions=tool_definitions, + tool_executor=tool_executor, + ) + + # TODO: Investigate if this method is still needed and remove if not. + async def process_message(self, + user_input: str, + session_id: Optional[str] = None, + response_length: str = "medium") -> Dict[str, Any]: + """ + Process a user message through the orchestration pipeline + """ + try: + # Get or create session + session = self.session_manager.get_session(session_id) + + # Add user message to session + session.append_message("user", user_input) + + # Determine if we need clarification + needs_clarification = self.needs_clarification(session, user_input) + + if needs_clarification: + # Generate clarification question + clarification = await self._generate_clarification_question(session) + session.append_message("system", f"Clarification request: {clarification}") + + return { + "status": "clarification_needed", + "message": clarification, + "suggestions": self._get_clarification_suggestions(), + "session_id": session.session_id + } + + # Generate responses from all personas + responses = await self.generate_persona_responses(session, response_length) + + return { + "status": "success", + "responses": responses, + "session_id": session.session_id + } + + except Exception as e: + logger.error(f"Error in process_message: {str(e)}") + return { + "status": "error", + "message": "I'm having technical difficulties. Please try again.", + "error": str(e) + } + + async def process_message_with_enhanced_context(self, user_input: str, session_id: str, response_length: str = "medium"): + """ + Enhanced message processing with document awareness and better context management + """ + try: + # Get session + session = self.session_manager.get_session(session_id) + + # Add user message to session + session.append_message("user", user_input) + + # Detect document references in the query + document_references = self._extract_document_references_from_query(user_input) + + # Get available documents for this session + rag_manager = get_rag_manager() + doc_stats = rag_manager.get_document_stats(session_id) + available_documents = [doc["filename"] for doc in doc_stats.get("documents", [])] + + # Generate enhanced persona responses + responses = await self.generate_persona_responses(session, response_length) + + return { + "status": "success", + "responses": responses, + "document_references_detected": bool(document_references), + "available_documents": available_documents, + "session_id": session_id + } + + except Exception as e: + logger.error(f"Error in enhanced message processing: {str(e)}") + return { + "status": "error", + "message": "I'm having technical difficulties processing your request.", + "suggestions": ["Please try rephrasing your question.", "Check if your documents uploaded successfully."] + } + + def _extract_document_references_from_query(self, query: str) -> List[str]: + """Extract document references from user query""" + query_lower = query.lower() + references = [] + + # Common document reference patterns + patterns = [ + r"(?:my|the|in)\s+([a-zA-Z_\-]+\.(?:pdf|docx|txt))", # specific files + r"(?:my|the)\s+(program|plan|routine|split|log|protocol|document|proposal|chapter|report)", # document types + r"(?:in|from)\s+(?:my\s+)?([a-zA-Z_\-\s]+(?:section|plan))", # sections + ] + + for pattern in patterns: + matches = re.findall(pattern, query_lower) + references.extend(matches) + + return references[:3] # Limit to first 3 references + + def _get_user_messages(self, session: ConversationContext) -> List[Dict[str, str]]: + return [msg for msg in session.messages if msg.get("role") == "user"] + + def _has_continuing_conversation(self, session: ConversationContext) -> bool: + """True when the user is past the first turn or advisors have already replied.""" + user_messages = self._get_user_messages(session) + if len(user_messages) > 1: + return True + advisor_roles = set(self.personas.keys()) | {"assistant", "orchestrator"} + return any(msg.get("role") in advisor_roles for msg in session.messages) + + def _prior_conversation_text( + self, session: ConversationContext, exclude_latest_user: bool = False + ) -> str: + """Concatenate prior messages for clarification and routing context.""" + messages = session.messages + if exclude_latest_user and messages and messages[-1].get("role") == "user": + messages = messages[:-1] + return "\n".join( + f"{msg.get('role', 'unknown')}: {msg.get('content', '')}" + for msg in messages + if msg.get("role") != "system" + ) + + def _is_follow_up_data_reply( + self, session: ConversationContext, user_input: str + ) -> bool: + """ + Detect short replies that answer a prior question (e.g. weight after + a protein question) even when session history failed to hydrate. + """ + user_lower = user_input.lower().strip() + if len(user_input.split()) > 10: + return False + + looks_like_data = any( + re.search(pattern, user_lower, re.IGNORECASE) + for pattern in _FOLLOW_UP_DATA_PATTERNS + ) + if not looks_like_data: + return False + + prior_text = self._prior_conversation_text( + session, exclude_latest_user=True + ).lower() + if not prior_text: + return False + + return any(keyword in prior_text for keyword in _FOLLOW_UP_TOPIC_KEYWORDS) + + def needs_clarification(self, session: ConversationContext, user_input: str) -> bool: + """ + Determine if the user input needs clarification. + Patterns and keywords are driven by config.yaml โ†’ orchestrator section. + """ + # TODO: This method should be refactored to be more generic instead of + # relying on hard-coded regex and keywords. + + if self._has_continuing_conversation(session): + logger.info("Skipping clarification: continuing conversation") + return False + + if self._is_follow_up_data_reply(session, user_input): + logger.info( + "NO CLARIFICATION: follow-up data reply with prior topic context" + ) + return False + + # Check for vague patterns - FIXED to handle "I am" vs "I'm" + vague_patterns = [ + r"^(help|advice|guidance|assistance)$", + r"i'?m (stuck|lost|confused|not sure)", # matches "I'm confused" + r"i am (stuck|lost|confused|not sure)", # matches "I am confused" + r"(what should i|how do i|where do i start)", + r"i need (help|advice|guidance)", + r"(any|some) (advice|suggestions|ideas)", + r"don'?t know (what|how|where)", + r"(stuck|struggling) with", + r"unsure about" + ] + + orch_cfg = get_settings().orchestrator + user_lower = user_input.lower().strip() + word_count = len(user_input.split()) + + logger.info(f"Checking clarification for: {user_input} ({word_count} words)") + + has_specific_keywords = any( + keyword in user_lower for keyword in orch_cfg.specific_keywords + ) + if has_specific_keywords: + logger.info("NO CLARIFICATION: input contains specific keywords") + return False + + if word_count >= orch_cfg.min_words_without_keywords: + logger.info( + f"NO CLARIFICATION: input has {word_count} words " + f"(>= {orch_cfg.min_words_without_keywords} threshold)") + return False + + for pattern in vague_patterns: + if re.search(pattern, user_lower): + logger.info(f"CLARIFICATION TRIGGERED: pattern `{pattern}` matched `{user_input}`") + return True + + logger.info("CLARIFICATION TRIGGERED: short input (%d words) without specific keywords", word_count) + return True + + async def needs_clarification_improved(self, session: ConversationContext, user_input: str) -> bool: + """ + Use an LLM call to determine whether the user's input is too vague + to route to the advisor panel. Falls back to the legacy rule-based + method if the LLM call fails. + """ + if self._has_continuing_conversation(session): + logger.info("Skipping clarification: continuing conversation") + return False + + if self._is_follow_up_data_reply(session, user_input): + logger.info( + "Skipping clarification: follow-up data reply with prior topic context" + ) + return False + + app_cfg = get_settings().app + orch_cfg = get_settings().orchestrator + advisor_descriptions = ", ".join( + f"{p.name} ({p.id})" for p in self.personas.values() + ) + domain_keywords = ", ".join(orch_cfg.specific_keywords) + conversation_context = self._prior_conversation_text( + session, exclude_latest_user=True + ) + + system_prompt = ( + "You are a routing classifier for an AI advisory application.\n\n" + f"Application: {app_cfg.title} โ€” {app_cfg.subtitle}\n" + f"Available advisors: {advisor_descriptions}\n" + f"Domain-relevant topics: {domain_keywords}\n\n" + "Your task: decide whether the user's latest message contains enough " + "substance to send to the advisors, or whether it is too vague and " + "requires a clarifying follow-up before the advisors can help.\n\n" + "Use the full conversation history when provided. Short follow-up " + "replies that supply requested data (body weight, schedule, equipment, " + "experience level) are CLEAR ENOUGH when prior messages asked for that " + "information or discussed a related topic (e.g. protein question then " + "'230 lbs' โ†’ ~198 g/day at 1.9 g/kg midpoint).\n\n" + "A message NEEDS CLARIFICATION when it:\n" + "- Expresses confusion or uncertainty without a concrete topic\n" + "- Is a single generic request like 'help' or 'advice'\n" + "- Contains no identifiable subject the advisors could address\n" + "- Is a standalone data point with NO prior conversation context\n\n" + "A message is CLEAR ENOUGH when it:\n" + "- Mentions a specific topic, question, or problem area\n" + "- Provides enough context for at least one advisor to respond usefully\n" + "- Even a short message is fine if the intent is unambiguous " + "(e.g. 'explain transformers' is clear)\n" + "- Messages mentioning domain-relevant topics are likely clear enough " + "to route directly, even if brief\n" + "- Is a follow-up answer that completes information from earlier turns\n\n" + "Respond ONLY with valid JSON:\n" + '{"needs_clarification": true or false, "reason": "one sentence explanation"}' + ) + + if conversation_context.strip(): + user_prompt = ( + f"Conversation so far:\n{conversation_context}\n\n" + f'Latest user message: "{user_input}"' + ) + else: + user_prompt = f'User message: "{user_input}"' + + raw = None + + try: + llm = next(iter(self.personas.values())).llm + raw = await llm.generate( + system_prompt=system_prompt, + context=[{"role": "user", "content": user_prompt}], + temperature=0.0, + max_tokens=128, + response_mime_type="application/json", + ) + + parsed = json.loads(raw.strip()) + value = parsed.get("needs_clarification") + if not isinstance(value, bool): + raise TypeError( + f"needs_clarification must be a boolean, got {type(value).__name__}: {value!r}" + ) + result = value + reason = parsed.get("reason", "") + + logger.info( + "LLM clarification classification: needs_clarification=%s, reason=%r, input=%r", + result, reason, user_input, + ) + return result + + except (json.JSONDecodeError, KeyError, TypeError) as exc: + logger.error("Failed to parse LLM classification response: %s (raw=%r)", exc, raw) + except Exception as exc: + logger.error("LLM classification call failed: %s", exc) + + # TODO: Evaluate if this fallback is still needed and remove if not. + logger.warning("Falling back to rule-based clarification check") + return self.needs_clarification(session, user_input) + + async def generate_contextual_clarification(self, user_input: str) -> Dict[str, Any]: + """ + Use the LLM to produce a clarification question and clickable + suggestions that are tailored to what the user actually typed. + Falls back to the static values in config.yaml if the LLM call fails. + """ + orch_cfg = get_settings().orchestrator + + advisor_list = ", ".join( + f"{p.name} ({p.id})" for p in self.personas.values() + ) + + system_prompt = ( + "You are a helpful routing assistant. The user's message is too " + "vague to send to the advisors. Produce a short clarifying question " + "and exactly 4 clickable suggestion buttons the user could press.\n\n" + "Reply ONLY with valid JSON โ€” no markdown, no extra text:\n" + '{"question": "...", "suggestions": ["...", "...", "...", "..."]}\n\n' + "Keep the question to one sentence. Each suggestion should be a " + "complete sentence the user could send as their next message." + ) + + user_prompt = ( + f"User said: \"{user_input}\"\n" + f"Available advisors: {advisor_list}\n\n" + "Generate a clarifying question and 4 suggestion buttons that " + "relate to what the user said and steer toward the advisors above." + ) + + try: + llm = next(iter(self.personas.values())).llm + raw = await llm.generate( + system_prompt=system_prompt, + context=[{"role": "user", "content": user_prompt}], + temperature=0.4, + max_tokens=1024, + response_mime_type="application/json" + ) + + cleaned = raw.strip() + cleaned = re.sub(r"```(?:json)?", "", cleaned).strip() + + json_match = re.search(r"\{.*\}", cleaned, re.DOTALL) + if json_match: + cleaned = json_match.group(0) + + parsed = json.loads(cleaned) + question = parsed.get("question", "").strip() + suggestions = parsed.get("suggestions", []) + + if question and isinstance(suggestions, list) and len(suggestions) >= 2: + logger.info(f"LLM clarification generated for: {user_input}") + return {"question": question, "suggestions": suggestions[:4]} + + except Exception as e: + logger.error(f"LLM clarification failed, using config fallback: {e}") + + fallback_questions = orch_cfg.clarification_questions + fallback_suggestions = orch_cfg.clarification_suggestions + return { + "question": fallback_questions[0], + "suggestions": fallback_suggestions, + } + + async def generate_persona_responses(self, session: ConversationContext, response_length: str = "medium"): + """ + Generate responses from all personas with enhanced RAG integration + """ + responses = [] + + for persona_id, persona in self.personas.items(): + logger.info(f"Generating response for {persona_id} with enhanced RAG") + + # Generate persona response with enhanced RAG + response_data = await self.generate_single_persona_response(session, persona, response_length) + + # Add persona response to session context + session.append_message(persona_id, response_data["response"]) + + responses.append(response_data) + + return responses + + async def generate_single_persona_response(self, session, persona, response_length: str = "medium"): + """ + Enhanced version - Generate response from a single persona with enhanced RAG integration + """ + try: + # Get the user's latest message for document retrieval + user_message = "" + try: + user_message = session.get_latest_user_message() or "" + except AttributeError: + # Fallback: manually find latest user message + for msg in reversed(session.messages): + if msg.get('role') == 'user': + user_message = msg.get('content', '') + break + + # Retrieve relevant document context using enhanced RAG + document_context = "" + if user_message: + document_context = await self._retrieve_relevant_documents( + user_input=user_message, + session_id=session.session_id, + persona_id=persona.id + ) + + # Build enhanced context for the LLM + enhanced_context = await self._build_enhanced_context_for_persona( + session, persona, user_message, document_context + ) + + # Generate response with enhanced context + response = await persona.respond(enhanced_context, response_length) + + # Validate and improve response quality + if not self._is_valid_response(response, persona.id): + logger.warning(f"Invalid response from {persona.id}, using fallback") + response = self._get_persona_fallback(persona.id) + + # Track document usage for debugging + used_documents = bool(document_context and len(document_context.strip()) > 100) + document_chunks_used = document_context.count("[Source:") if document_context else 0 + + return { + "persona_id": persona.id, + "persona_name": persona.name, + "response": response, + "used_documents": used_documents, + "document_chunks_used": document_chunks_used, + "response_length": response_length, + "context_quality": "high" if document_context else "conversation_only" + } + + except Exception as e: + logger.error(f"Error generating response for {persona.id}: {str(e)}") + return { + "persona_id": persona.id, + "persona_name": persona.name, + "response": f"I apologize, but I'm having technical difficulties. {self._get_persona_fallback(persona.id)}", + "used_documents": False, + "document_chunks_used": 0, + "response_length": response_length, + "context_quality": "error" + } + + async def _retrieve_relevant_documents(self, user_input: str, session_id: str, persona_id: str = "") -> str: + """ + Enhanced document retrieval with document awareness and better attribution + """ + try: + # Add comprehensive logging to track session ID usage + logger.info(f"Retrieving documents for session_id: {session_id}") + logger.info(f"User input: {user_input[:100]}...") + + rag_manager = get_rag_manager() + + # Check what documents are available for this session with detailed logging + doc_stats = rag_manager.get_document_stats(session_id) + logger.info(f"Available documents for {session_id}: {doc_stats.get('total_documents', 0)} documents, {doc_stats.get('total_chunks', 0)} chunks") + + # Log document details for debugging + if doc_stats.get('documents'): + for doc in doc_stats['documents']: + logger.info(f" - Document: {doc.get('filename', 'unknown')} ({doc.get('chunks', 0)} chunks)") + + # If no documents found and this looks like a chat session, log warning + if doc_stats.get('total_documents', 0) == 0: + if session_id.startswith('chat_'): + logger.warning(f"No documents found for chat session {session_id} - this may indicate session ID mismatch during upload") + + # Try alternative session ID formats for debugging + alternative_formats = [ + session_id.replace('chat_', ''), # Remove chat_ prefix + session_id, # Keep as is + ] + + for alt_session_id in alternative_formats: + if alt_session_id != session_id: + alt_stats = rag_manager.get_document_stats(alt_session_id) + if alt_stats.get('total_documents', 0) > 0: + logger.warning(f"Found documents under alternative session ID {alt_session_id}: {alt_stats}") + else: + logger.info(f"No documents found for new session {session_id} - this is normal for new chats") + + return "" # No documents available + + # Extract document hints from user query + document_hint = self._extract_document_hint_from_query(user_input) + logger.info(f"Document hint extracted from query: {document_hint}") + + # Get persona-specific context for better retrieval + persona_context = self._get_enhanced_persona_context_keywords(persona_id) + + # Search for relevant chunks with document awareness + logger.info(f"Searching with persona context: {persona_context[:100]}...") + relevant_chunks = rag_manager.search_documents_with_context( + query=user_input, + session_id=session_id, + persona_context=persona_context, + n_results=6, # Increased for better context + document_hint=document_hint + ) + + logger.info(f"Retrieved {len(relevant_chunks)} chunks for {persona_id}") + + # Log relevance scores for debugging + if relevant_chunks: + for i, chunk in enumerate(relevant_chunks): + relevance = chunk.get("relevance_score", 0) + doc_source = chunk.get("document_source", {}) + filename = doc_source.get("filename", "unknown") + logger.info(f" Chunk {i+1}: {filename} (relevance: {relevance:.3f})") + + if not relevant_chunks: + logger.info(f"No relevant document chunks found for query: {user_input[:50]}...") + return "" + + # Format retrieved content with enhanced attribution + formatted_context = self._format_document_context_with_attribution(relevant_chunks, persona_id) + + # Log final context length + logger.info(f"Final document context length: {len(formatted_context)} characters") + + return formatted_context + + except Exception as e: + logger.error(f"Error retrieving documents for {persona_id} in session {session_id}: {str(e)}") + logger.error(f"Error type: {type(e).__name__}") + import traceback + logger.error(f"Full traceback: {traceback.format_exc()}") + return "" + + def _extract_document_hint_from_query(self, query: str) -> Optional[str]: + """ + Extract document name hints from user queries + """ + query_lower = query.lower() + + # Common patterns for document references + document_indicators = [ + r"(?:my|the|in|from)\s+([a-zA-Z_\-]+\.(?:pdf|docx|txt|doc))", # specific files + r"(?:my|the)\s+(program|plan|routine|split|log|protocol|document|proposal|chapter|report)", # document types + r"(?:in|from)\s+(?:my\s+)?([a-zA-Z_\-\s]+(?:section|plan|program))", # sections + r"(?:the|my)\s+([a-zA-Z_\-\s]+(?:document|file))", # generic documents + ] + + for pattern in document_indicators: + matches = re.findall(pattern, query_lower) + if matches: + return matches[0].strip().replace(" ", "_") + + return None + + def _get_enhanced_persona_context_keywords(self, persona_id: str) -> str: + """ + Enhanced persona-specific keywords for better document retrieval + """ + enhanced_keywords = { + "methodologist": "methodology research design experimental approach data collection sampling validity reliability statistical analysis quantitative qualitative mixed-methods procedures protocol IRB ethics", + "theorist": "theory theoretical framework conceptual model literature review philosophy epistemology ontology paradigm abstract concepts hypothesis proposition postulate axiom", + "pragmatist": "practical application implementation action steps next steps recommendation solution strategy timeline concrete advice roadmap execution deliverables milestones" + } + return enhanced_keywords.get(persona_id, "") + + def _format_document_context_with_attribution(self, chunks: List[Dict], persona_id: str) -> str: + """ + Format document context with clear attribution and source information + """ + if not chunks: + return "" + + # Filter chunks by relevance (increased threshold for quality) + high_quality_chunks = [ + chunk for chunk in chunks + if chunk.get("relevance_score", 0) > 0.4 # Increased from 0.3 + ] + + if not high_quality_chunks: + # If no high-quality chunks, take top 2 anyway but with lower confidence + high_quality_chunks = chunks[:2] + + formatted_sections = [] + + # Group chunks by document for better organization + documents = {} + for chunk in high_quality_chunks: + doc_source = chunk.get("document_source", {}) + filename = doc_source.get("filename", "unknown") + + if filename not in documents: + documents[filename] = { + "title": doc_source.get("document_title", filename), + "chunks": [] + } + documents[filename]["chunks"].append(chunk) + + # Format each document's content + for filename, doc_data in documents.items(): + doc_title = doc_data["title"] + doc_chunks = doc_data["chunks"] + + formatted_sections.append(f"=== FROM DOCUMENT: {doc_title} ===") + + for i, chunk in enumerate(doc_chunks): + doc_source = chunk.get("document_source", {}) + section = doc_source.get("section", "unknown section") + position = doc_source.get("chunk_position", "unknown position") + relevance = chunk.get("relevance_score", 0) + + chunk_intro = f"[Source: {section}, Part {position}, Relevance: {relevance:.2f}]" + formatted_sections.append(f"{chunk_intro}\n{chunk['text']}\n") + + # Add context summary + total_docs = len(documents) + total_chunks = len(high_quality_chunks) + + context_header = f""" +DOCUMENT CONTEXT FOR {persona_id.upper()} ANALYSIS: +Found {total_chunks} relevant passages from {total_docs} document(s). +Use this context to inform your response, and cite specific documents when referencing information. + +""" + + formatted_context = context_header + "\n".join(formatted_sections) + + # Add instructions specific to persona + persona_instructions = self._get_persona_document_instructions(persona_id) + formatted_context += f"\n\nSPECIAL INSTRUCTIONS FOR {persona_id.upper()}:\n{persona_instructions}" + + return formatted_context + + def _get_persona_document_instructions(self, persona_id: str) -> str: + """ + Get persona-specific instructions for handling document context + """ + instructions = { + "methodologist": """ +When analyzing the document context: +- Focus on methodological rigor and research design elements +- Identify potential validity threats or methodological gaps +- Suggest specific improvements to research procedures +- Reference exact methodological frameworks mentioned in their documents +- Connect their approach to established research standards""", + + "theorist": """ +When analyzing the document context: +- Examine theoretical positioning and conceptual clarity +- Identify theoretical gaps or inconsistencies +- Suggest theoretical frameworks that align with their work +- Evaluate the coherence between theory and research questions +- Reference specific theoretical concepts mentioned in their documents""", + + "pragmatist": """ +When analyzing the document context: +- Extract actionable next steps from their current progress +- Identify immediate bottlenecks or decision points +- Prioritize tasks based on their timeline and constraints +- Translate theoretical concepts into practical implementation steps +- Reference specific deadlines or milestones mentioned in their documents""" + } + + return instructions.get(persona_id, "Provide helpful guidance based on the document context.") + + async def _build_enhanced_context_for_persona(self, session, persona, user_message: str, document_context: str) -> List[Dict[str, str]]: + """ + Build enhanced context that properly integrates document information with conversation history + FIXED VERSION - Ensures document context is properly preserved for both providers + """ + enhanced_context = [] + + conversation_messages = [ + m for m in session.messages if m.get("role") != "system" + ] + history_tokens = self.context_manager._estimate_tokens_for_messages( + conversation_messages + ) + threshold = get_settings().orchestrator.conversation_history_token_threshold + + # Check if we actually have meaningful document content + has_documents = bool(document_context and document_context.strip() and len(document_context.strip()) > 50) + + # Build the system message with proper document awareness + if has_documents: + # Get list of uploaded documents + uploaded_docs = session.uploaded_files if hasattr(session, 'uploaded_files') else [] + doc_list = ", ".join(uploaded_docs) if uploaded_docs else "uploaded documents" + + system_message = f"""{persona.system_prompt} + + CURRENT SESSION CONTEXT: + The user has uploaded the following documents: {doc_list} + + DOCUMENT CONTENT: + {document_context} + + IMPORTANT: When the user refers to "my document," "my plan," "my program," etc., they are referring to one of their uploaded documents. Use the document context above to understand which specific document they mean and reference it by name in your response. + + Always cite your sources when referencing information from their documents using the format: "According to your [document_name]..." or "In your [section_name] from [document_name]..." + """ + else: + # NO DOCUMENTS - Explicitly tell persona not to reference documents + system_message = f"""{persona.system_prompt} + + IMPORTANT: The user has NOT uploaded any documents yet. Do not reference any specific documents, files, or assume you have access to their materials. + + If they mention "my document," "my plan," "my program," etc., you should: + 1. Acknowledge that you don't have access to their specific documents + 2. Ask them to upload the relevant files for more targeted advice + 3. Provide general guidance based on best practices in your area of expertise + + Do NOT make up document names or pretend to have access to files that don't exist.""" + + if hasattr(session, "user_profile_context") and session.user_profile_context: + system_message += ( + f"\n\n{session.user_profile_context}\n" + "Use this background to calibrate technical depth, examples, and priorities." + ) + + computed_nutrition = compute_protein_advisory_context(conversation_messages) + if computed_nutrition: + system_message += ( + f"\n\n{format_computed_context_block(computed_nutrition)}" + ) + + enhanced_context.append({ + "role": "system", + "content": system_message, + }) + + if history_tokens < threshold: + for message in conversation_messages: + enhanced_context.append({ + "role": message["role"], + "content": message["content"], + }) + else: + msg_count = len(conversation_messages) + if ( + session.conversation_summary + and session.conversation_summary_message_count == msg_count + ): + summary = session.conversation_summary + else: + llm = self.llm_client + if llm is None and self.personas: + llm = next(iter(self.personas.values())).llm + persona_names = {p.id: p.name for p in self.personas.values()} + summary = "" + if llm is not None: + summary = await generate_conversation_context_summary( + conversation_messages, + llm, + persona_names=persona_names, + ) + if summary: + session.conversation_summary = summary + session.conversation_summary_message_count = msg_count + system_message += f"\n\nSummary of earlier conversation:\n{summary}" + enhanced_context[0]["content"] = system_message + else: + logger.warning( + "Summary generation failed; including full history (%d tokens)", + history_tokens, + ) + for message in conversation_messages: + enhanced_context.append({ + "role": message["role"], + "content": message["content"], + }) + + return enhanced_context + + def _is_valid_response(self, response: str, persona_id: str) -> bool: + """Validate response quality""" + if len(response) < 10 or len(response) > 5000: + return False + + # Check for AI confusion indicators + confusion_indicators = [ + f"Thank you, Dr. {persona_id.title()}", + "Assistant:", + f"Dr. {persona_id.title()} Advisor:", + "excellent discussion, Assistant" + ] + + return not any(indicator in response for indicator in confusion_indicators) + + def _get_persona_fallback(self, persona_id: str) -> str: + """Get persona-specific fallback responses""" + fallbacks = { + "methodologist": "I'd be happy to help with your research methodology. What specific methodological approach are you considering?", + "theorist": "I'd like to explore the theoretical foundation of your work. What conceptual framework guides your research?", + "pragmatist": "Let's take a practical approach. What's the most pressing decision you need to make about your research right now?" + } + return fallbacks.get(persona_id, "I'd be happy to help. Could you provide more specific details about your question?") + + def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]: + """Get information about a session""" + session = self.session_manager.get_session(session_id) + if session: + return { + "session_id": session.session_id, + "message_count": len(session.messages), + "uploaded_files": session.uploaded_files, + "created_at": session.created_at.isoformat(), + "last_accessed": session.last_accessed.isoformat(), + "context_summary": self.context_manager.get_context_summary(session.messages) + } + return None + + def reset_session(self, session_id: str) -> bool: + """Reset a session (clear messages but keep metadata)""" + session = self.session_manager.get_session(session_id) + if session: + session.clear_messages() + return True + return False + + def delete_session(self, session_id: str) -> bool: + """Delete a session completely""" + return self.session_manager.delete_session(session_id) + + # Legacy method for backward compatibility + def _get_persona_context_keywords(self, persona_id: str) -> str: + """ + Legacy method - use _get_enhanced_persona_context_keywords instead + """ + return self._get_enhanced_persona_context_keywords(persona_id) + + async def chat_with_persona(self, user_input: str, persona_id: str, session_id: str, response_length: str = "medium") -> Dict[str, Any]: + """ + Chat with a specific persona directly - FIXED for consistent document access + """ + try: + persona = self.get_persona(persona_id) + if not persona: + return { + "error": f"Persona {persona_id} not found", + "available_personas": list(self.personas.keys()), + "persona_id": persona_id, + "persona_name": "Unknown" + } + + # Ensure session exists and log session info + session = self.session_manager.get_session(session_id) + logger.info(f"Chat with {persona_id} using session {session_id}") + + # Add user message to session + session.append_message("user", user_input) + + # Use the same session_id for document retrieval + logger.info(f"Generating response for {persona_id} with session {session_id}") + + # Generate response from single persona using consistent session ID + response_data = await self.generate_single_persona_response(session, persona, response_length) + + # Add response to session + session.append_message(persona_id, response_data["response"]) + + # Ensure response data includes all necessary fields + return { + "persona_id": persona_id, + "persona_name": persona.name, + "response": response_data.get("response", "I'm having trouble generating a response."), + "used_documents": response_data.get("used_documents", False), + "document_chunks_used": response_data.get("document_chunks_used", 0), + "response_length": response_length, + "context_quality": response_data.get("context_quality", "unknown"), + "session_id": session_id, + "type": "single_persona_response", + "persona": { + "persona_id": persona_id, + "persona_name": persona.name, + "response": response_data.get("response", "I'm having trouble generating a response."), + "used_documents": response_data.get("used_documents", False), + "document_chunks_used": response_data.get("document_chunks_used", 0) + } + } + + except Exception as e: + logger.error(f"Error in chat_with_persona for {persona_id}: {str(e)}") + logger.error(f"Session ID: {session_id}") + import traceback + logger.error(f"Full traceback: {traceback.format_exc()}") + + return { + "error": f"Error processing request: {str(e)}", + "persona_id": persona_id, + "persona_name": self.personas.get(persona_id, {}).name if persona_id in self.personas else "Unknown", + "response": "I encountered an error while processing your request. Please try again.", + "used_documents": False, + "document_chunks_used": 0, + "response_length": response_length, + "context_quality": "error", + "session_id": session_id, + "type": "error" + } + + + async def get_top_personas( + self, + session_id: str, + k: int = 3, + candidate_ids: Optional[List[str]] = None, + ) -> List[str]: + """ + Use the LLM to rank personas based on current session context. + Falls back to default persona order if LLM fails or returns invalid data. + + When ``candidate_ids`` is provided (e.g., from the header's advisor + selection dropdown) ranking is restricted to that pool so users only + receive responses from advisors they've enabled. + """ + try: + session = self.session_manager.get_session(session_id) + + if not self.personas: + logger.warning("No personas registered.") + return [] + + if candidate_ids: + candidate_personas = { + pid: self.personas[pid] + for pid in candidate_ids + if pid in self.personas + } + if not candidate_personas: + candidate_personas = dict(self.personas) + else: + candidate_personas = dict(self.personas) + + # Use the LLM from one of the existing persona objects + llm = next(iter(candidate_personas.values())).llm + + # Use recent conversation context (last 5 messages) + recent_context = "\n".join( + msg['content'] for msg in session.get_recent_messages(5) + ) + + # Format available persona descriptions (only the candidate pool) + persona_descriptions = "\n".join([ + f"- ID: {p.id}\n Name: {p.name}\n Prompt: {p.system_prompt.strip()}" + for p in candidate_personas.values() + ]) + + # Ensure k does not exceed the number of candidate personas + k = min(k, len(candidate_personas)) + + app_title = get_settings().app.title + + prompt = f""" + The user is seeking advice from {app_title}. Based on the conversation below, choose the top {k} most relevant advisors. + + Respond ONLY with a JSON list of exactly {k} advisor IDs in order of relevance. + Example response: ["methodist", "pragmatist", "theorist"] + + --- Conversation --- + {recent_context} + + --- Available Advisors --- + {persona_descriptions} + """.strip() + + llm_response = await llm.generate( + system_prompt=f"You are an assistant that selects the best advisors for a user of {app_title}.", + context=[{"role": "user", "content": prompt}], + temperature=0.4, + max_tokens=150, + response_mime_type="application/json" + ) + + # Step 1: Try direct JSON load + try: + top_ids = json.loads(llm_response.strip()) + except json.JSONDecodeError: + # Step 2: Fallback: try extracting list of quoted strings + top_ids = re.findall(r'"(.*?)"', llm_response) + logger.warning(f"Fallback JSON extraction used: {top_ids}") + + # Handle models that wrap the list in an object (e.g. {"advisor_ids": [...]}) + if isinstance(top_ids, dict): + top_ids = next(iter(top_ids.values()), []) + + # Step 3: Filter valid persona IDs against the candidate pool + valid_ids = [pid for pid in top_ids if pid in candidate_personas] + + if len(valid_ids) < k: + logger.warning(f"LLM returned insufficient or invalid IDs. Got: {valid_ids}") + return list(candidate_personas.keys())[:k] + + return valid_ids[:k] + + except Exception as e: + logger.error(f"Error selecting top personas: {e}") + if candidate_ids: + fallback_ids = [pid for pid in candidate_ids if pid in self.personas] + if fallback_ids: + return fallback_ids[:k] + return list(self.personas.keys())[:k] diff --git a/multi_llm_chatbot_backend/app/core/onboarding_agent.py b/multi_llm_chatbot_backend/app/core/onboarding_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..054e5f5df9ba60c25bffe819919246ab2c709d26 --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/onboarding_agent.py @@ -0,0 +1,183 @@ +""" +OnboardingAgent โ€” conversational profile gathering for fitness advisors. +""" + +import json +import logging +import re +from datetime import datetime +from typing import Any, Dict, List, Optional + +from app.core.database import get_database + +LOG = logging.getLogger(__name__) + +# NOTE: field keys are kept stable (knowledge_level, cyber_role, ...) for storage +# compatibility with the user profile schema; the questions/descriptions below +# are themed for a fitness / bodybuilding advisor panel. +PROFILE_FIELDS: List[tuple] = [ + ("knowledge_level", "What is your fitness experience level?", + "Level such as beginner, intermediate, or advanced"), + ("timezone", "What time zone are you usually in?", + "IANA timezone or region such as America/New_York, Europe/London, UTC"), + ("cyber_role", "What best describes your training focus right now?", + "Focus such as general fitness, bodybuilding/hypertrophy, powerlifting/strength, cutting, or athlete"), + ("organization_type", "Where do you usually train?", + "Commercial gym, home gym, campus gym, CrossFit box, outdoor/calisthenics, or hybrid"), + ("primary_domains", "Which muscle groups or areas do you focus on most?", + "Comma-separated areas such as chest, back, legs, arms, core, conditioning"), + ("certifications", "What equipment do you have access to?", + "List such as dumbbells, barbell, cables, bands, machines, or bodyweight only"), + ("tools_stack", "What apps or trackers do you use regularly?", + "MyFitnessPal, Strong, Hevy, Apple Health, a spreadsheet, etc."), + ("compliance_focus", "Any dietary approach or restrictions we should know about?", + "High-protein, vegetarian/vegan, cutting, bulking, allergies, or none"), + ("current_goals", "What are you trying to accomplish in the next few months?", + "Muscle gain, fat loss, strength PRs, first pull-up, recomposition, etc."), + ("learning_preferences", "How do you prefer to train?", + "Full-body, upper/lower or bro splits, supersets, progressive overload, etc."), +] + + +class OnboardingAgent: + def __init__(self, llm: Any) -> None: + self.llm = llm + + SKIP_SENTINEL = "__skipped__" + + @staticmethod + def _field_has_value(val: Any) -> bool: + if val is None: + return False + if isinstance(val, str): + return bool(val.strip()) + if isinstance(val, list): + return len(val) > 0 + return bool(val) + + async def chat(self, user_input: str, user_id: Any, existing_profile: Dict[str, Any]) -> Dict[str, Any]: + filled = {k for k, _, _d in PROFILE_FIELDS + if self._field_has_value(existing_profile.get(k))} + missing = [(k, q, desc) for k, q, desc in PROFILE_FIELDS if k not in filled] + completion = int(len(filled) / len(PROFILE_FIELDS) * 100) + current_field = missing[0][0] if missing else None + + extracted = await self._extract_fields(user_input, missing, current_field) + + db = get_database() + if extracted: + from app.api.routes.user_profile import _normalize_field + real_values: Dict[str, Any] = {} + skipped_keys: set = set() + for k, v in extracted.items(): + if v == self.SKIP_SENTINEL: + skipped_keys.add(k) + elif v: + real_values[k] = _normalize_field(k, v) + + update: Dict[str, Any] = {} + if real_values: + update.update(real_values) + for sk in skipped_keys: + update[sk] = "" + + if update: + update["updated_at"] = datetime.utcnow() + await db.user_profiles.update_one( + {"user_id": user_id}, + {"$set": update, "$setOnInsert": {"user_id": user_id}}, + upsert=True, + ) + filled.update(real_values.keys()) + filled.update(skipped_keys) + missing = [(k, q, desc) for k, q, desc in PROFILE_FIELDS if k not in filled] + completion = int(len(filled) / len(PROFILE_FIELDS) * 100) + + if not missing: + return { + "reply": "Great โ€” your fitness profile is complete. Your coaches will tailor workouts, " + "nutrition, and next steps to your goals, equipment, and experience.", + "progress": 100, + "complete": True, + } + + reply = await self._generate_next_question(user_input, existing_profile, filled, missing) + return {"reply": reply, "progress": completion, "complete": False} + + async def _extract_fields( + self, text: str, missing_fields: List[tuple], current_field: Optional[str] = None + ) -> Dict[str, Any]: + if not text.strip(): + return {} + skip_instruction = "" + if current_field: + skip_instruction = ( + f'\nThe question just asked was about "{current_field}". ' + "If the user declines, refuses, says they don't know, says skip, " + f'return {{"{current_field}": "__skipped__"}}.' + ) + field_descriptions = "\n".join(f'- "{k}": {desc}' for k, _q, desc in missing_fields) + system = ( + "Extract fitness profile fields from the user's message. " + "Return ONLY valid JSON with field names as keys. " + "For list fields return a JSON array. " + f"{skip_instruction}\n" + f"Fields:\n{field_descriptions}" + ) + try: + raw = await self.llm.generate( + system_prompt=system, + context=[{"role": "user", "content": text}], + temperature=0.1, + max_tokens=512, + ) + cleaned = re.sub(r"```(?:json)?", "", raw).strip() + m = re.search(r"\{.*\}", cleaned, re.DOTALL) + if m: + return json.loads(m.group(0)) + except Exception as e: + LOG.warning(f"Extraction failed: {e}") + return {} + + async def _generate_next_question( + self, user_input: str, profile: Dict[str, Any], filled: set, missing: List[tuple] + ) -> str: + filled_parts: List[str] = [] + for k in filled: + val = profile.get(k) + if val: + filled_parts.append(f"{k}={val}") + else: + filled_parts.append(f"{k}=DECLINED") + filled_summary = ", ".join(filled_parts) or "nothing yet" + next_field_key, next_field_q, _ = missing[0] + system = ( + "You are a friendly fitness onboarding assistant. " + "You help users build a profile so AI fitness coaches can personalize workouts and nutrition.\n" + "RULES:\n" + "- Respond in exactly ONE short paragraph (2-3 sentences).\n" + "- Briefly acknowledge what they said, then ask ONE clear question.\n" + "- End with a question mark.\n" + "- No headings or labels like 'Question:'.\n" + "- If they skipped a topic, say 'No problem!' and move on.\n" + "- Never repeat topics already gathered or declined." + ) + user_prompt = ( + f"User just said: \"{user_input}\"\n" + f"Already gathered: {filled_summary}\n" + f"Next topic: {next_field_key} โ€” {next_field_q}\n" + "Write a warm response that acknowledges them and asks about the next topic." + ) + try: + reply = await self.llm.generate( + system_prompt=system, + context=[{"role": "user", "content": user_prompt}], + temperature=0.7, + max_tokens=300, + ) + if reply and "?" not in reply: + reply = f"{reply} {next_field_q}" + return reply + except Exception as e: + LOG.error(f"Question generation failed: {e}") + return missing[0][1] if missing else "Tell me more about your fitness background!" diff --git a/multi_llm_chatbot_backend/app/core/rag_manager.py b/multi_llm_chatbot_backend/app/core/rag_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..e27b347379a0a1939c12ebe2d42ff0d9326d2b76 --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/rag_manager.py @@ -0,0 +1,981 @@ +import chromadb +from chromadb.config import Settings +from sentence_transformers import SentenceTransformer +import nltk +import tiktoken +from typing import List, Dict, Any, Optional +import uuid +import logging +import os +import re +from pathlib import Path + +logger = logging.getLogger(__name__) + + +def _default_chroma_path(subdir: str) -> str: + """Resolve the default ChromaDB persistence directory. + + On HF Spaces (and any deployment that sets ``DATA_DIR``) the embeddings + must live on the bucket mount so they survive Space rebuilds. In local + dev DATA_DIR is unset, so we keep the historical working-directory path + so existing local installs and tests don't move their data. + """ + data_dir = os.environ.get("DATA_DIR", "").strip() + if data_dir: + return str(Path(data_dir) / "chroma" / subdir) + return f"./{subdir}" + +# Download required NLTK data +try: + nltk.data.find('tokenizers/punkt') +except LookupError: + try: + nltk.download('punkt') + except Exception as e: + logger.warning(f"Could not download NLTK punkt: {e}") + +class DocumentChunker: + """Intelligent document chunking with overlaps and semantic boundaries""" + + def __init__(self, chunk_size: int = 500, overlap: int = 50): + self.chunk_size = chunk_size + self.overlap = overlap + try: + self.encoding = tiktoken.get_encoding("cl100k_base") # GPT-4 encoding + except Exception as e: + logger.warning(f"Could not load tiktoken encoding: {e}") + self.encoding = None + + def _count_tokens(self, text: str) -> int: + """Count tokens in text""" + if self.encoding: + return len(self.encoding.encode(text)) + else: + # Fallback: approximate 4 chars per token + return len(text) // 4 + + def _encode_text(self, text: str) -> List[int]: + """Encode text to tokens""" + if self.encoding: + return self.encoding.encode(text) + else: + # Fallback: return character indices + return list(range(len(text))) + + def _decode_tokens(self, tokens: List[int]) -> str: + """Decode tokens back to text""" + if self.encoding: + return self.encoding.decode(tokens) + else: + # Fallback: can't properly decode without tiktoken + return "" + + def chunk_text(self, text: str, metadata: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + Chunk text intelligently with semantic boundaries + """ + # Clean and normalize text + text = self._clean_text(text) + + # Simple sentence splitting fallback if NLTK isn't available + try: + sentences = nltk.sent_tokenize(text) + except Exception: + # Fallback sentence splitting + sentences = re.split(r'[.!?]+', text) + sentences = [s.strip() for s in sentences if s.strip()] + + chunks = [] + current_chunk = "" + current_tokens = 0 + chunk_index = 0 + + for sentence in sentences: + sentence = sentence.strip() + if not sentence: + continue + + sentence_tokens = self._count_tokens(sentence) + + # If adding this sentence would exceed chunk size, save current chunk + if current_tokens + sentence_tokens > self.chunk_size and current_chunk: + chunk_data = self._create_chunk_metadata( + current_chunk.strip(), + metadata, + chunk_index + ) + chunks.append(chunk_data) + + # Start new chunk with overlap + overlap_text = self._get_overlap_text(current_chunk) + current_chunk = overlap_text + " " + sentence + current_tokens = self._count_tokens(current_chunk) + chunk_index += 1 + else: + current_chunk += " " + sentence + current_tokens += sentence_tokens + + # Add final chunk if it has content + if current_chunk.strip(): + chunk_data = self._create_chunk_metadata( + current_chunk.strip(), + metadata, + chunk_index + ) + chunks.append(chunk_data) + + logger.info(f"Created {len(chunks)} chunks from document: {metadata.get('filename', 'unknown')}") + return chunks + + def _clean_text(self, text: str) -> str: + """Clean and normalize text""" + # Remove excessive whitespace and newlines + text = re.sub(r'\n+', '\n', text) + text = re.sub(r'\s+', ' ', text) + return text.strip() + + def _get_overlap_text(self, text: str) -> str: + """Get the last N tokens for overlap""" + if self.encoding: + tokens = self._encode_text(text) + if len(tokens) <= self.overlap: + return text + + overlap_tokens = tokens[-self.overlap:] + return self._decode_tokens(overlap_tokens) + else: + # Fallback: use character-based overlap + words = text.split() + overlap_words = words[-self.overlap:] if len(words) > self.overlap else words + return " ".join(overlap_words) + + def _create_chunk_metadata(self, chunk_text: str, base_metadata: Dict[str, Any], chunk_index: int) -> Dict[str, Any]: + """Create comprehensive metadata for a chunk""" + return { + "text": chunk_text, + "chunk_id": str(uuid.uuid4()), + "chunk_index": chunk_index, + "token_count": self._count_tokens(chunk_text), + "filename": base_metadata.get("filename", "unknown"), + "file_type": base_metadata.get("file_type", "unknown"), + "session_id": base_metadata.get("session_id"), + "upload_timestamp": base_metadata.get("upload_timestamp"), + "file_size": base_metadata.get("file_size", 0) + } + +class SimpleEmbeddingFunction: + """Simple embedding function wrapper for ChromaDB - FIXED for new interface""" + + def __init__(self, model): + self.model = model + + def __call__(self, input): + """Generate embeddings for input texts - Updated signature for ChromaDB 0.4.16+""" + try: + # Handle both single string and list of strings + if isinstance(input, str): + input = [input] + + embeddings = self.model.encode(input, convert_to_tensor=False) + return embeddings.tolist() if hasattr(embeddings, 'tolist') else embeddings + except Exception as e: + logger.error(f"Error generating embeddings: {e}") + # Return dummy embeddings as fallback + return [[0.0] * 384 for _ in input] # 384 is dimension for all-MiniLM-L6-v2 + +class RAGManager: + """ + Retrieval-Augmented Generation Manager - FIXED VERSION + + Handles document storage, embedding, and retrieval using ChromaDB + """ + + def __init__(self, embedding_model: str = None, persist_directory: str = None): + from app.config import get_settings + settings = get_settings() + if embedding_model is None: + embedding_model = settings.rag.embedding_model + self.embedding_model_name = embedding_model + if persist_directory is None: + persist_directory = _default_chroma_path("chroma_db") + self.persist_directory = Path(persist_directory) + self.persist_directory.mkdir(parents=True, exist_ok=True) + + # Initialize embedding model + logger.info(f"Loading embedding model: {embedding_model}") + try: + self.embedding_model = SentenceTransformer(embedding_model) + logger.info("Embedding model loaded successfully") + except Exception as e: + logger.error(f"Failed to load embedding model: {e}") + raise + + # Initialize ChromaDB client + try: + self.client = chromadb.PersistentClient( + path=str(self.persist_directory), + settings=Settings( + anonymized_telemetry=False, + allow_reset=True + ) + ) + logger.info("ChromaDB client initialized") + except Exception as e: + logger.error(f"Failed to initialize ChromaDB client: {e}") + raise + + # Initialize collection โ€” name from config + self.collection_name = settings.rag.chroma_collection + self.collection = self._get_or_create_collection() + + # Initialize chunker + self.chunker = DocumentChunker() + + def _get_or_create_collection(self): + """Get or create the ChromaDB collection""" + try: + # First, try to get existing collection + try: + collection = self.client.get_collection( + name=self.collection_name + ) + logger.info(f"Found existing collection: {self.collection_name}") + return collection + except ValueError: + # Collection doesn't exist, create it + logger.info(f"Creating new collection: {self.collection_name}") + collection = self.client.create_collection( + name=self.collection_name, + embedding_function=SimpleEmbeddingFunction(self.embedding_model), + metadata={"description": f"{settings.app.title} document storage"} + ) + logger.info(f"Created collection: {self.collection_name}") + return collection + + except Exception as e: + logger.error(f"Error with collection management: {e}") + # Try to reset and recreate + try: + logger.info("Attempting to reset and recreate collection...") + self.client.reset() + collection = self.client.create_collection( + name=self.collection_name, + embedding_function=SimpleEmbeddingFunction(self.embedding_model), + metadata={"description": f"{settings.app.title} document storage"} + ) + logger.info("Successfully recreated collection") + return collection + except Exception as e2: + logger.error(f"Failed to recreate collection: {e2}") + raise + + def add_document(self, content: str, filename: str, session_id: str, file_type: str = "unknown") -> Dict[str, Any]: + """ + Add a document to the vector database + """ + try: + # Create base metadata + base_metadata = { + "filename": filename, + "file_type": file_type, + "session_id": session_id, + "upload_timestamp": str(uuid.uuid4()), # Using UUID as timestamp for simplicity + "file_size": len(content) + } + + # Chunk the document + chunks = self.chunker.chunk_text(content, base_metadata) + + if not chunks: + raise ValueError("No chunks created from document") + + # Prepare data for ChromaDB + chunk_ids = [chunk["chunk_id"] for chunk in chunks] + chunk_texts = [chunk["text"] for chunk in chunks] + chunk_metadatas = [ + {k: v for k, v in chunk.items() if k != "text" and k != "chunk_id"} + for chunk in chunks + ] + + # Add to ChromaDB with error handling + try: + self.collection.add( + ids=chunk_ids, + documents=chunk_texts, + metadatas=chunk_metadatas + ) + logger.info(f"Successfully added {len(chunks)} chunks for document: {filename}") + except Exception as e: + logger.error(f"Error adding chunks to ChromaDB: {e}") + # Try to recreate collection and try again + self.collection = self._get_or_create_collection() + self.collection.add( + ids=chunk_ids, + documents=chunk_texts, + metadatas=chunk_metadatas + ) + logger.info(f"Successfully added {len(chunks)} chunks after collection reset") + + return { + "success": True, + "filename": filename, + "chunks_created": len(chunks), + "total_tokens": sum(chunk["token_count"] for chunk in chunks), + "chunk_ids": chunk_ids + } + + except Exception as e: + logger.error(f"Error adding document {filename}: {str(e)}") + return { + "success": False, + "filename": filename, + "error": str(e) + } + + def search_documents(self, query: str, session_id: str, persona_context: str = "", n_results: int = 5) -> List[Dict[str, Any]]: + """ + Search for relevant document chunks + """ + try: + # Enhance query with persona context for better retrieval + enhanced_query = f"{query} {persona_context}".strip() + + # Search the collection + results = self.collection.query( + query_texts=[enhanced_query], + n_results=n_results, + where={"session_id": session_id} # Filter by session + ) + + # Format results + retrieved_chunks = [] + if results['documents'] and results['documents'][0]: + for i, (doc, metadata, distance) in enumerate(zip( + results['documents'][0], + results['metadatas'][0], + results['distances'][0] + )): + # ChromaDB returns squared euclidean distance, convert to similarity + # For better similarity scores, we'll use: similarity = 1 / (1 + distance) + similarity_score = 1 / (1 + abs(distance)) if distance is not None else 0.5 + + chunk_data = { + "text": doc, + "metadata": metadata, + "relevance_score": similarity_score, + "distance": distance, # Keep original for debugging + "rank": i + 1 + } + retrieved_chunks.append(chunk_data) + + logger.info(f"Retrieved {len(retrieved_chunks)} chunks for query: {query[:50]}...") + return retrieved_chunks + + except Exception as e: + logger.error(f"Error searching documents: {str(e)}") + return [] + + def get_document_stats(self, session_id: str) -> Dict[str, Any]: + """Get statistics about documents in a session""" + try: + # Get all chunks for this session + results = self.collection.get( + where={"session_id": session_id} + ) + + if not results or not results.get('metadatas'): + return { + "total_chunks": 0, + "total_documents": 0, + "documents": [] + } + + # Analyze metadata + metadatas = results['metadatas'] + documents = {} + + for metadata in metadatas: + filename = metadata.get('filename', 'unknown') + if filename not in documents: + documents[filename] = { + "filename": filename, + "file_type": metadata.get('file_type', 'unknown'), + "chunk_count": 0, + "total_tokens": 0 + } + + documents[filename]["chunk_count"] += 1 + documents[filename]["total_tokens"] += metadata.get('token_count', 0) + + return { + "total_chunks": len(metadatas), + "total_documents": len(documents), + "documents": list(documents.values()) + } + + except Exception as e: + logger.error(f"Error getting document stats: {str(e)}") + return {"total_chunks": 0, "total_documents": 0, "documents": []} + + def delete_session_documents(self, session_id: str) -> bool: + """Delete all documents for a session""" + try: + # Get all document IDs for this session + results = self.collection.get( + where={"session_id": session_id} + ) + + if results and results.get('ids'): + chunk_ids = results['ids'] + self.collection.delete(ids=chunk_ids) + logger.info(f"Deleted {len(chunk_ids)} chunks for session: {session_id}") + return True + + return True # No documents to delete is also success + + except Exception as e: + logger.error(f"Error deleting session documents: {str(e)}") + return False + + def health_check(self) -> Dict[str, Any]: + """Check if RAG system is working properly""" + try: + # Test basic operations + test_doc = "This is a test document for health checking." + test_session = "health_check_session" + + # Try adding a test document + result = self.add_document(test_doc, "test.txt", test_session, "txt") + if not result["success"]: + return {"status": "error", "message": "Failed to add test document"} + + # Try searching + search_results = self.search_documents("test", test_session) + + # Try getting stats + stats = self.get_document_stats(test_session) + + # Cleanup + self.delete_session_documents(test_session) + + return { + "status": "healthy", + "embedding_model": self.embedding_model_name, + "collection_name": self.collection_name, + "test_results": { + "add_document": result["success"], + "search_documents": len(search_results) > 0, + "get_stats": stats["total_chunks"] > 0 + } + } + + except Exception as e: + return {"status": "error", "message": str(e)} + + +class EnhancedRAGManager: + def __init__(self, persist_directory: str = None): + """Initialize enhanced RAG manager with improved document handling""" + from app.config import get_settings + settings = get_settings() + + if persist_directory is None: + persist_directory = _default_chroma_path("chromadb_storage") + self.persist_directory = persist_directory + Path(persist_directory).mkdir(parents=True, exist_ok=True) + + # Initialize ChromaDB client + self.client = chromadb.PersistentClient( + path=persist_directory, + settings=Settings(anonymized_telemetry=False) + ) + + # Collection name from config + collection_name = settings.rag.chroma_collection + + # Create or get collection + self.collection = self.client.get_or_create_collection( + name=collection_name, + metadata={"hnsw:space": "cosine"} + ) + + logger.info(f"Enhanced RAG Manager initialized with collection: {self.collection.name}") + + def add_document(self, content: str, filename: str, session_id: str, + file_type: str = "unknown") -> Dict[str, Any]: + """ + Enhanced document addition with better metadata and document awareness + """ + try: + # Preprocess the content + cleaned_content = self._preprocess_content(content) + if not cleaned_content.strip(): + return { + "success": False, + "error": "Document content is empty after preprocessing", + "filename": filename + } + + # Extract document metadata + doc_metadata = self._extract_document_metadata(cleaned_content, filename, file_type) + + # Create intelligent chunks with overlap and context preservation + chunks = self._create_enhanced_chunks(cleaned_content, filename, doc_metadata) + + # Prepare data for ChromaDB + chunk_texts = [] + chunk_metadatas = [] + chunk_ids = [] + + for i, chunk_data in enumerate(chunks): + chunk_id = f"{session_id}_{filename}_{i}_{uuid.uuid4().hex[:8]}" + + # Enhanced metadata with document awareness + metadata = { + "session_id": session_id, + "filename": filename, + "file_type": file_type, + "chunk_index": i, + "total_chunks": len(chunks), + "document_section": chunk_data.get("section", "unknown"), + "keywords": chunk_data.get("keywords", ""), + "chunk_type": chunk_data.get("type", "content"), + "document_title": doc_metadata.get("title", filename), + "estimated_tokens": len(chunk_data["text"].split()) * 1.3, + "has_references": "references" in chunk_data["text"].lower(), + "has_methodology": "method" in chunk_data["text"].lower(), + "has_theory": any(word in chunk_data["text"].lower() + for word in ["theory", "theoretical", "framework", "concept"]) + } + + chunk_texts.append(chunk_data["text"]) + chunk_metadatas.append(metadata) + chunk_ids.append(chunk_id) + + # Add to ChromaDB + self.collection.add( + documents=chunk_texts, + metadatas=chunk_metadatas, + ids=chunk_ids + ) + + total_tokens = sum(metadata["estimated_tokens"] for metadata in chunk_metadatas) + + logger.info(f"Successfully added document {filename}: {len(chunks)} chunks, ~{total_tokens:.0f} tokens") + + return { + "success": True, + "filename": filename, + "chunks_created": len(chunks), + "total_tokens": int(total_tokens), + "document_metadata": doc_metadata + } + + except Exception as e: + logger.error(f"Error adding document {filename}: {str(e)}") + return { + "success": False, + "filename": filename, + "error": str(e) + } + + def search_documents_with_context(self, query: str, session_id: str, + persona_context: str = "", n_results: int = 5, + document_hint: str = None) -> List[Dict[str, Any]]: + """ + Enhanced search with document awareness and context + """ + try: + # Extract potential document references from query + document_references = self._extract_document_references(query) + + # Build enhanced query + enhanced_query = self._build_enhanced_query(query, persona_context, document_references) + + # Base search filters + search_filters = {"session_id": session_id} + + # If specific document mentioned, prioritize it + if document_hint or document_references: + priority_filename = document_hint or document_references[0] if document_references else None + if priority_filename: + # First search: prioritize specific document + priority_results = self._search_with_filters( + enhanced_query, + {**search_filters, "filename": {"$contains": priority_filename}}, + n_results=min(3, n_results) + ) + + # Second search: get additional context from other documents + remaining_results = max(0, n_results - len(priority_results)) + if remaining_results > 0: + general_results = self._search_with_filters( + enhanced_query, + search_filters, + n_results=remaining_results + 2 # Get extras to filter out duplicates + ) + + # Combine results, avoiding duplicates + all_results = priority_results + [ + r for r in general_results + if r["metadata"]["filename"] != priority_filename + ][:remaining_results] + else: + all_results = priority_results + else: + all_results = self._search_with_filters(enhanced_query, search_filters, n_results) + else: + all_results = self._search_with_filters(enhanced_query, search_filters, n_results) + + # Enhance results with context and attribution + enhanced_results = self._enhance_search_results(all_results, query) + + logger.info(f"Enhanced search returned {len(enhanced_results)} results for query: {query[:50]}...") + return enhanced_results + + except Exception as e: + logger.error(f"Error in enhanced document search: {str(e)}") + return [] + + def _search_with_filters(self, query: str, filters: Dict, n_results: int) -> List[Dict[str, Any]]: + """Helper method for filtered search""" + results = self.collection.query( + query_texts=[query], + n_results=n_results, + where=filters + ) + + formatted_results = [] + if results['documents'] and results['documents'][0]: + for i, (doc, metadata, distance) in enumerate(zip( + results['documents'][0], + results['metadatas'][0], + results['distances'][0] + )): + similarity_score = 1 / (1 + abs(distance)) if distance is not None else 0.5 + + formatted_results.append({ + "text": doc, + "metadata": metadata, + "relevance_score": similarity_score, + "distance": distance, + "rank": i + 1 + }) + + return formatted_results + + def _extract_document_references(self, query: str) -> List[str]: + """Extract potential document name references from user query""" + # Common patterns for document references + patterns = [ + r"(?:my|the|in)\s+([a-zA-Z_\-]+\.(?:pdf|docx|txt|doc))", # my document.pdf + r"(?:my|the)\s+(dissertation|thesis|proposal|chapter|paper|manuscript)", # my dissertation + r"(?:in|from)\s+(?:my\s+)?([a-zA-Z_\-\s]+(?:chapter|section|proposal))", # in my methodology chapter + r"(?:the|my)\s+([a-zA-Z_\-\s]+(?:document|file))", # the research document + ] + + references = [] + query_lower = query.lower() + + for pattern in patterns: + matches = re.findall(pattern, query_lower) + references.extend(matches) + + # Clean up references + cleaned_references = [] + for ref in references: + cleaned = ref.strip().replace(" ", "_") + if len(cleaned) > 2: # Avoid single characters + cleaned_references.append(cleaned) + + return cleaned_references[:3] # Limit to first 3 references + + def _build_enhanced_query(self, original_query: str, persona_context: str, + document_refs: List[str]) -> str: + """Build enhanced query with context and document awareness""" + query_parts = [original_query] + + if persona_context: + query_parts.append(persona_context) + + if document_refs: + query_parts.extend(document_refs) + + return " ".join(query_parts) + + def _enhance_search_results(self, results: List[Dict], original_query: str) -> List[Dict[str, Any]]: + """Enhance search results with better attribution and context""" + enhanced = [] + + for result in results: + metadata = result["metadata"] + + # Create enhanced result with clear attribution + enhanced_result = { + **result, + "document_source": { + "filename": metadata.get("filename", "unknown"), + "document_title": metadata.get("document_title", metadata.get("filename", "unknown")), + "section": metadata.get("document_section", "unknown"), + "chunk_position": f"{metadata.get('chunk_index', 0) + 1} of {metadata.get('total_chunks', 1)}" + }, + "content_type": metadata.get("chunk_type", "content"), + "context_indicators": { + "has_methodology": metadata.get("has_methodology", False), + "has_theory": metadata.get("has_theory", False), + "has_references": metadata.get("has_references", False) + } + } + + enhanced.append(enhanced_result) + + # Sort by relevance score, but boost results from explicitly mentioned documents + enhanced.sort(key=lambda x: ( + 1.0 if any(ref in x["document_source"]["filename"].lower() + for ref in self._extract_document_references(original_query)) else 0.0, + x["relevance_score"] + ), reverse=True) + + return enhanced + + def _extract_document_metadata(self, content: str, filename: str, file_type: str) -> Dict[str, Any]: + """Extract metadata from document content""" + lines = content.split('\n') + + # Try to find title (usually first significant line) + title = filename + for line in lines[:10]: + if line.strip() and len(line.strip()) > 10 and len(line.strip()) < 100: + if not line.strip().startswith(('Abstract', 'Introduction', '1.', 'Chapter')): + title = line.strip() + break + + # Extract other metadata + word_count = len(content.split()) + has_sections = bool(re.search(r'(?:Chapter|Section|\d+\.)', content)) + + return { + "title": title, + "word_count": word_count, + "has_sections": has_sections, + "file_type": file_type, + "estimated_pages": word_count // 250 # Rough estimate + } + + def _create_enhanced_chunks(self, content: str, filename: str, doc_metadata: Dict) -> List[Dict[str, Any]]: + """Create intelligent chunks with context preservation""" + # Split into logical sections first + sections = self._split_into_sections(content) + + chunks = [] + for section_data in sections: + section_text = section_data["text"] + section_type = section_data["type"] + + # Create overlapping chunks within each section + if len(section_text.split()) > 300: # Large section, needs chunking + section_chunks = self._create_overlapping_chunks(section_text, chunk_size=250, overlap=50) + for chunk_text in section_chunks: + chunks.append({ + "text": chunk_text, + "section": section_type, + "type": "content", + "keywords": self._extract_keywords(chunk_text) + }) + else: + # Small section, keep as single chunk + chunks.append({ + "text": section_text, + "section": section_type, + "type": section_type, + "keywords": self._extract_keywords(section_text) + }) + + return chunks + + def _split_into_sections(self, content: str) -> List[Dict[str, Any]]: + """Split content into logical sections""" + lines = content.split('\n') + sections = [] + current_section = [] + current_type = "introduction" + + for line in lines: + line = line.strip() + if not line: + continue + + # Check if this line starts a new section + section_match = re.match(r'(?:Chapter|Section|\d+\.?\s+)(.+)', line, re.IGNORECASE) + if section_match: + # Save previous section + if current_section: + sections.append({ + "text": '\n'.join(current_section), + "type": current_type + }) + + # Start new section + current_section = [line] + section_title = section_match.group(1).lower() + current_type = self._classify_section_type(section_title) + else: + current_section.append(line) + + # Add final section + if current_section: + sections.append({ + "text": '\n'.join(current_section), + "type": current_type + }) + + return sections if sections else [{"text": content, "type": "content"}] + + def _classify_section_type(self, section_title: str) -> str: + """Classify section type based on title""" + title_lower = section_title.lower() + + if any(word in title_lower for word in ['method', 'approach', 'design', 'procedure']): + return "methodology" + elif any(word in title_lower for word in ['theory', 'framework', 'literature', 'review']): + return "theory" + elif any(word in title_lower for word in ['result', 'finding', 'analysis', 'data']): + return "results" + elif any(word in title_lower for word in ['conclusion', 'discussion', 'implication']): + return "conclusion" + elif any(word in title_lower for word in ['introduction', 'background', 'abstract']): + return "introduction" + else: + return "content" + + def _create_overlapping_chunks(self, text: str, chunk_size: int = 250, overlap: int = 50) -> List[str]: + """Create overlapping chunks from text""" + words = text.split() + chunks = [] + + start = 0 + while start < len(words): + end = min(start + chunk_size, len(words)) + chunk = ' '.join(words[start:end]) + chunks.append(chunk) + + if end >= len(words): + break + + start += chunk_size - overlap + + return chunks + + def _extract_keywords(self, text: str) -> str: + """Extract key terms from text chunk""" + # Simple keyword extraction - could be enhanced with NLP + words = text.lower().split() + + # Academic keywords to prioritize + academic_terms = set([ + 'methodology', 'theory', 'analysis', 'research', 'study', 'data', + 'framework', 'approach', 'method', 'findings', 'results', 'literature', + 'hypothesis', 'experiment', 'survey', 'interview', 'observation', + 'qualitative', 'quantitative', 'mixed-methods', 'case study' + ]) + + found_keywords = [word for word in words if word in academic_terms] + return ' '.join(found_keywords[:5]) # Top 5 keywords + + def _preprocess_content(self, content: str) -> str: + """Clean and preprocess document content""" + # Remove excessive whitespace + content = re.sub(r'\s+', ' ', content) + + # Remove page numbers and headers/footers + content = re.sub(r'\n\s*\d+\s*\n', '\n', content) + + # Clean up encoding issues + content = content.replace('\ufffd', ' ') + + return content.strip() + + def get_document_stats(self, session_id: str) -> Dict[str, Any]: + """Get enhanced statistics about documents in a session""" + try: + # Get all chunks for this session + results = self.collection.get( + where={"session_id": session_id}, + include=["metadatas"] + ) + + if not results['metadatas']: + return {"total_chunks": 0, "total_documents": 0, "documents": []} + + # Analyze documents + documents = {} + total_tokens = 0 + + for metadata in results['metadatas']: + filename = metadata.get('filename', 'unknown') + + if filename not in documents: + documents[filename] = { + "filename": filename, + "title": metadata.get('document_title', filename), + "file_type": metadata.get('file_type', 'unknown'), + "chunks": 0, + "estimated_tokens": 0, + "sections": set(), + "has_methodology": False, + "has_theory": False, + "has_references": False + } + + doc_info = documents[filename] + doc_info["chunks"] += 1 + doc_info["estimated_tokens"] += metadata.get('estimated_tokens', 0) + doc_info["sections"].add(metadata.get('document_section', 'unknown')) + + if metadata.get('has_methodology'): + doc_info["has_methodology"] = True + if metadata.get('has_theory'): + doc_info["has_theory"] = True + if metadata.get('has_references'): + doc_info["has_references"] = True + + total_tokens += metadata.get('estimated_tokens', 0) + + # Convert sets to lists for JSON serialization + for doc_info in documents.values(): + doc_info["sections"] = list(doc_info["sections"]) + + return { + "total_chunks": len(results['metadatas']), + "total_documents": len(documents), + "total_estimated_tokens": int(total_tokens), + "documents": list(documents.values()) + } + + except Exception as e: + logger.error(f"Error getting document stats: {str(e)}") + return {"error": str(e), "total_chunks": 0, "total_documents": 0} + + +# Global RAG manager instance +_rag_manager = None + +# def get_rag_manager() -> RAGManager: +# """Get the global RAG manager instance""" +# global _rag_manager +# if _rag_manager is None: +# try: +# _rag_manager = RAGManager() +# logger.info("RAG Manager initialized successfully") +# except Exception as e: +# logger.error(f"Failed to initialize RAG Manager: {e}") +# raise +# return _rag_manager + +def get_rag_manager() -> EnhancedRAGManager: + """Get or create the global RAG manager instance""" + global _rag_manager + if _rag_manager is None: + _rag_manager = EnhancedRAGManager() + return _rag_manager \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/core/seamless_orchestrator.py b/multi_llm_chatbot_backend/app/core/seamless_orchestrator.py new file mode 100644 index 0000000000000000000000000000000000000000..953b05c078d1df1d82cd890879805c07575f3b72 --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/seamless_orchestrator.py @@ -0,0 +1,204 @@ +# app/core/seamless_orchestrator.py + +import re +from typing import Dict, List, Optional +from app.llm.llm_client import LLMClient + +class SeamlessOrchestrator: + def __init__(self, llm: LLMClient = None): + self.llm = llm + self.required_fields = ["research_area", "specific_question", "academic_stage"] + self.collected_info = {} + self.conversation_history = [] + self.orchestrator_active = False + + def is_input_complete(self) -> bool: + """Check if we have enough information to proceed to advisors""" + return all(field in self.collected_info for field in self.required_fields) + + def is_input_vague(self, user_input: str) -> bool: + """Determine if the input is too vague and needs clarification""" + vague_patterns = [ + r"i'm (not sure|unsure|confused|lost)", + r"i (don't know|dunno) (what|how|where)", + r"help me with (my|the|a) (thesis|research|phd)", + r"(what should i|how do i|where do i start)", + r"i need (help|advice|guidance)", + r"(stuck|struggling) with", + r"(any|some) (advice|suggestions|ideas)", + r"^(help|advice|guidance)$", + ] + + user_lower = user_input.lower().strip() + + # Check for vague patterns + for pattern in vague_patterns: + if re.search(pattern, user_lower): + return True + + # Check if input is too short (likely vague) + if len(user_input.split()) < 8: + return True + + return False + + def extract_info(self, user_input: str) -> Dict[str, str]: + """Extract information from user input""" + info = {} + user_lower = user_input.lower() + + # Detect research area + research_areas = { + "computer science": ["computer science", "cs", "software", "programming", "algorithms"], + "machine learning": ["machine learning", "ml", "ai", "artificial intelligence", "neural networks"], + "biology": ["biology", "bio", "genetics", "molecular", "cell"], + "physics": ["physics", "quantum", "mechanics", "particle"], + "psychology": ["psychology", "psych", "cognitive", "behavioral"], + "chemistry": ["chemistry", "chemical", "organic", "inorganic"], + "mathematics": ["mathematics", "math", "statistics", "calculus"], + "engineering": ["engineering", "mechanical", "electrical", "civil"], + "economics": ["economics", "finance", "business", "market"], + "literature": ["literature", "english", "writing", "poetry"], + "history": ["history", "historical", "ancient", "medieval"], + "sociology": ["sociology", "social", "society", "culture"] + } + + for area, keywords in research_areas.items(): + if any(keyword in user_lower for keyword in keywords): + info["research_area"] = area + break + + # Detect academic stage + stage_patterns = { + "first year": ["first year", "1st year", "beginning", "just started", "new"], + "second year": ["second year", "2nd year", "coursework", "classes"], + "third year": ["third year", "3rd year", "qualifying", "comprehensive"], + "dissertation": ["dissertation", "thesis", "writing", "final year", "defense"], + "postdoc": ["postdoc", "post-doctoral", "finished", "graduated"] + } + + for stage, keywords in stage_patterns.items(): + if any(keyword in user_lower for keyword in keywords): + info["academic_stage"] = stage + break + + # Detect specific question types + question_keywords = { + "topic selection": ["topic", "research question", "focus", "direction", "what to study"], + "methodology": ["method", "approach", "design", "analysis", "how to research"], + "literature review": ["literature", "sources", "papers", "reading", "references"], + "time management": ["time", "schedule", "deadline", "planning", "organizing"], + "advisor relationship": ["advisor", "supervisor", "mentor", "committee", "meeting"], + "career planning": ["career", "job", "postdoc", "industry", "academic job"], + "writing": ["writing", "paper", "manuscript", "publication"], + "funding": ["funding", "grant", "scholarship", "money", "financial"] + } + + for q_type, keywords in question_keywords.items(): + if any(keyword in user_lower for keyword in keywords): + info["specific_question"] = q_type + break + + return info + + def analyze_input(self, user_input: str): + """Analyze and store information from user input""" + self.conversation_history.append({"role": "user", "content": user_input}) + extracted = self.extract_info(user_input) + self.collected_info.update(extracted) + + async def generate_orchestrator_question(self) -> str: + """Generate a contextual follow-up question using the LLM""" + if not self.llm: + return self._get_fallback_question() + + missing = [f for f in self.required_fields if f not in self.collected_info] + + # Create context for the LLM + context = f"Conversation so far: {' '.join([msg['content'] for msg in self.conversation_history])}\n" + context += f"Information collected: {self.collected_info}\n" + context += f"Still need to know: {missing}\n" + + system_prompt = """You are a PhD advisor's assistant. Ask ONE brief follow-up question (under 15 words) to gather missing info. + + Missing info types: + - research_area: What field/discipline they study + - specific_question: What specific aspect they need help with + - academic_stage: What stage of PhD they're in + + Ask briefly and naturally.""" + + try: + question = await self.llm.generate(system_prompt, [{"role": "user", "content": context}], temperature=0.5, max_tokens=50) + return question.strip() + except Exception as e: + print(f"Error generating orchestrator question: {e}") + return self._get_fallback_question() + + def _get_fallback_question(self) -> str: + """Fallback questions when LLM fails""" + missing = [f for f in self.required_fields if f not in self.collected_info] + fallback_questions = { + "research_area": "What field are you studying in?", + "specific_question": "What specific aspect would you like guidance on?", + "academic_stage": "What stage of your PhD are you currently in?" + } + return fallback_questions.get(missing[0] if missing else "research_area", "Could you tell me more about your situation?") + + async def process_message(self, user_input: str) -> Dict: + """Process user message and determine next action""" + + # If this is the first message and it's vague, activate orchestrator + if not self.conversation_history and self.is_input_vague(user_input): + self.orchestrator_active = True + + # Analyze the input + self.analyze_input(user_input) + + # If orchestrator is active and we don't have complete info + if self.orchestrator_active and not self.is_input_complete(): + question = await self.generate_orchestrator_question() + return { + "status": "orchestrator_asking", + "orchestrator_question": question, + "collected_info": self.collected_info.copy() + } + + # If we have complete information or orchestrator was never activated + if self.is_input_complete() or not self.orchestrator_active: + self.orchestrator_active = False + return { + "status": "ready_for_advisors", + "collected_info": self.collected_info.copy(), + "enhanced_context": self._create_enhanced_context(user_input) + } + + # Default case - proceed without orchestrator + return { + "status": "ready_for_advisors", + "collected_info": {}, + "enhanced_context": user_input + } + + def _create_enhanced_context(self, original_query: str) -> str: + """Create enhanced context for advisors""" + if not self.collected_info: + return original_query + + context_parts = [] + if "research_area" in self.collected_info: + context_parts.append(f"Research area: {self.collected_info['research_area']}") + if "academic_stage" in self.collected_info: + context_parts.append(f"Academic stage: {self.collected_info['academic_stage']}") + if "specific_question" in self.collected_info: + context_parts.append(f"Focus area: {self.collected_info['specific_question']}") + + if context_parts: + return f"[Context: {', '.join(context_parts)}] {original_query}" + return original_query + + def reset(self): + """Reset for new conversation""" + self.collected_info = {} + self.conversation_history = [] + self.orchestrator_active = False \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/core/session_manager.py b/multi_llm_chatbot_backend/app/core/session_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..6d4e4d47602191b2c851b6b08ca6b190aa679c7c --- /dev/null +++ b/multi_llm_chatbot_backend/app/core/session_manager.py @@ -0,0 +1,226 @@ +from typing import Dict, List, Optional, Any +from datetime import datetime, timedelta +import uuid +from dataclasses import dataclass, field +import asyncio +from threading import Lock +from app.core.rag_manager import get_rag_manager + +@dataclass +class ConversationContext: + """Enhanced conversation context for RAG integration""" + + def __init__(self, session_id: str = None): + self.session_id = session_id or str(uuid.uuid4()) + self.messages: List[Dict[str, str]] = [] + self.uploaded_files: List[str] = [] # Now just stores filenames, not content + self.total_upload_size: int = 0 # For tracking purposes only + self.created_at = datetime.now() + self.last_accessed = datetime.now() + + # New RAG-related attributes + self.document_chunks_count: int = 0 # Track total chunks in vector DB + self.last_retrieval_stats: Dict[str, Any] = {} # Last RAG retrieval info + + # Cached LLM summary when conversation history exceeds token threshold + self.conversation_summary: Optional[str] = None + self.conversation_summary_message_count: int = 0 + + def clear_conversation_summary(self) -> None: + self.conversation_summary = None + self.conversation_summary_message_count = 0 + + def append_message(self, role: str, content: str): + """Add a message to the conversation history""" + self.messages.append({ + "role": role, + "content": content, + "timestamp": datetime.now().isoformat() + }) + self.last_accessed = datetime.now() + + def clear_messages(self): + """Clear conversation messages but keep document references""" + self.messages.clear() + self.clear_conversation_summary() + self.last_accessed = datetime.now() + + def get_messages_by_role(self, role: str) -> List[Dict[str, str]]: + """Get all messages by a specific role - ADDED METHOD""" + return [msg for msg in self.messages if msg.get('role') == role] + + def get_recent_messages(self, count: int = 10) -> List[Dict[str, str]]: + """Get the most recent N messages""" + return self.messages[-count:] if len(self.messages) > count else self.messages + + def get_user_messages(self) -> List[Dict[str, str]]: + """Get all user messages""" + return self.get_messages_by_role('user') + + def get_latest_user_message(self) -> Optional[str]: + """Get the content of the most recent user message""" + user_messages = self.get_user_messages() + return user_messages[-1]['content'] if user_messages else None + + def add_uploaded_file(self, filename: str, content: str, file_size: int): + """ + Add uploaded file - MODIFIED FOR RAG + + Now we only track the filename and size in session context. + The actual content goes to the vector database via RAG manager. + """ + self.uploaded_files.append(filename) + self.total_upload_size += file_size + + # Add a system message noting the upload (not the full content) + self.append_message("system", f"Document '{filename}' uploaded and processed into vector database") + + # Update document chunk count from RAG manager + try: + rag_manager = get_rag_manager() + stats = rag_manager.get_document_stats(self.session_id) + self.document_chunks_count = stats.get("total_chunks", 0) + except Exception as e: + print(f"Warning: Could not update chunk count: {e}") + + def get_context_size(self) -> int: + """Calculate conversation context size in characters (excluding vector DB documents)""" + return sum(len(msg['content']) for msg in self.messages) + + def get_rag_stats(self) -> Dict[str, Any]: + """Get statistics about documents in vector database for this session""" + try: + rag_manager = get_rag_manager() + return rag_manager.get_document_stats(self.session_id) + except Exception as e: + return {"error": str(e), "total_chunks": 0, "total_documents": 0} + + def clear_all_data(self): + """Clear both conversation and vector database documents""" + # Clear conversation messages + self.clear_messages() + + # Clear vector database documents + try: + rag_manager = get_rag_manager() + success = rag_manager.delete_session_documents(self.session_id) + if success: + self.uploaded_files.clear() + self.total_upload_size = 0 + self.document_chunks_count = 0 + self.append_message("system", "All conversation history and documents cleared") + else: + self.append_message("system", "Conversation cleared, but some documents may remain") + except Exception as e: + self.append_message("system", f"Conversation cleared, document cleanup failed: {str(e)}") + +class SessionManager: + """Thread-safe session manager for handling multiple user conversations""" + + def __init__(self, session_timeout_hours: int = 24, cleanup_interval_minutes: int = 60): + self.sessions: Dict[str, ConversationContext] = {} + self.session_timeout = timedelta(hours=session_timeout_hours) + self.cleanup_interval = timedelta(minutes=cleanup_interval_minutes) + self.lock = Lock() + self.last_cleanup = datetime.now() + + def create_session(self) -> str: + """Create a new session and return session ID""" + session_id = str(uuid.uuid4()) + with self.lock: + self.sessions[session_id] = ConversationContext(session_id=session_id) + return session_id + + def get_session(self, session_id: Optional[str] = None) -> ConversationContext: + """Get existing session or create new one""" + if not session_id: + session_id = self.create_session() + + with self.lock: + if session_id not in self.sessions: + self.sessions[session_id] = ConversationContext(session_id=session_id) + + session = self.sessions[session_id] + session.last_accessed = datetime.now() + + # Trigger cleanup if needed + self._cleanup_expired_sessions() + + return session + + def delete_session(self, session_id: str) -> bool: + """Delete a specific session""" + with self.lock: + if session_id in self.sessions: + del self.sessions[session_id] + return True + return False + + def get_active_session_count(self) -> int: + """Get number of active sessions""" + with self.lock: + return len(self.sessions) + + def _cleanup_expired_sessions(self): + """Remove expired sessions (called periodically)""" + now = datetime.now() + + # Only run cleanup periodically to avoid overhead + if now - self.last_cleanup < self.cleanup_interval: + return + + expired_sessions = [] + for session_id, session in self.sessions.items(): + if now - session.last_accessed > self.session_timeout: + expired_sessions.append(session_id) + + for session_id in expired_sessions: + del self.sessions[session_id] + + self.last_cleanup = now + + if expired_sessions: + print(f"Cleaned up {len(expired_sessions)} expired sessions") + + def reset_session_completely(self, session_id: str) -> bool: + """ + Completely reset a session - both conversation and vector database documents + """ + with self.lock: + if session_id in self.sessions: + session = self.sessions[session_id] + session.clear_all_data() + return True + return False + + def get_session_stats(self, session_id: str) -> Dict[str, Any]: + """Get comprehensive session statistics including RAG data""" + with self.lock: + if session_id not in self.sessions: + return {"error": "Session not found"} + + session = self.sessions[session_id] + + # Get basic session stats + basic_stats = { + "session_id": session_id, + "message_count": len(session.messages), + "uploaded_files": session.uploaded_files, + "total_upload_size": session.total_upload_size, + "context_size_chars": session.get_context_size(), + "created_at": session.created_at.isoformat(), + "last_accessed": session.last_accessed.isoformat() + } + + # Get RAG stats + rag_stats = session.get_rag_stats() + + # Combine stats + return {**basic_stats, "rag_stats": rag_stats} + +# Global session manager instance +session_manager = SessionManager() + +def get_session_manager() -> SessionManager: + """Get the global session manager instance""" + return session_manager \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/llm/README.md b/multi_llm_chatbot_backend/app/llm/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d4be8c4f140a81838dfeb3d1a0aec3d14b746b6a --- /dev/null +++ b/multi_llm_chatbot_backend/app/llm/README.md @@ -0,0 +1,173 @@ +# `app/llm` โ€“ LLM Integration Layer + +This module abstracts and implements communication with **local** and **cloud-based** large language models (LLMs) via interchangeable client wrappers. + +It defines: +- A common interface for all LLM clients (`LLMClient`) +- A wrapper for Google Gemini API (`ImprovedGeminiClient`) +- A wrapper for Ollama local models (`ImprovedOllamaClient`) +- A sentence transformer embedding model (`embedding_client.py`) + +--- + +## Abstract Base โ€“ `llm_client.py` + +This file defines the **contract** that all LLM clients must follow. + +### `class LLMClient (ABC)` + +An abstract base class using Pythonโ€™s `abc` module. + +```python +@abstractmethod +async def generate(system_prompt: str, context: List[dict], temperature: float, max_tokens: int) -> str +``` + +Every model wrapper must implement this coroutine to generate a response given: +- A system prompt (persona instructions) +- A user/system message context (list of `{role, content}` dicts) +- A temperature (float 0.0โ€“1.0, typically scaled from 0โ€“10) +- A token limit (integer) + +--- + +## Gemini Client โ€“ `improved_gemini_client.py` + +### Overview + +- Communicates with **Googleโ€™s Gemini API** via `httpx` +- Dynamically injects the `system_prompt` into the context using `context_manager` +- Uses environment variables for API key and model name (`GEMINI_API_KEY`, `GEMINI_MODEL`) + +### Key Features + +| Feature | Description | +|--------|-------------| +| Context Prep | Uses `context_manager.prepare_context_for_llm()` to optimize message length | +| Endpoint | `https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent` | +| Content Format | Gemini expects JSON-formatted `contents`, not string prompts | +| Safety Settings | Blocks harmful or explicit content categories | +| Fallback Logic | Returns user-friendly error messages on bad or empty responses | +| Token Limit | `maxOutputTokens` passed explicitly | + +### SafetyConfig JSON Example + +```json +"safetySettings": [ + {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, + {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"} +] +``` + +### Differences from Ollama +- Requires an API key and runs over HTTPS +- Parses deeply nested JSON structures (candidates โ†’ content โ†’ parts) +- Strict token and safety controls +- More structured response format + +--- + +## Ollama Client โ€“ `improved_ollama_client.py` + +### Overview + +- Interfaces with a **local Ollama model server** (`http://localhost:11434`) +- Sends prompts as raw formatted strings (not JSON "messages") +- Uses `context_manager` to prepare prompt text + +### Key Features + +| Feature | Description | +|--------|-------------| +| Endpoint | `/api/generate` | +| Payload | Flat prompt string + generation config | +| Cleansing | Strips verbose, inconsistent prefixes or filler | +| Quality Filter | Removes overly verbose or vague responses | +| Robust | Recovers from connection and timeout failures | + +### Prompt Payload Example + +```json +{ + "model": "llama3.2:1b", + "prompt": "System: You are a helpful advisor...\nUser: What is...", + "stream": false, + "options": { + "temperature": 0.4, + "top_p": 0.9, + "top_k": 40, + "num_predict": 300, + "repeat_penalty": 1.1, + "stop": ["Student:", "User:", "Question:"] + } +} +``` + +### Differences from Gemini + +| Area | Gemini | Ollama | +|------|--------|--------| +| Hosting | Cloud API | Local server | +| Format | JSON "messages" | Raw string prompt | +| Safety Filters | Yes | No | +| Token Control | `maxOutputTokens` | `num_predict` | +| Output | Structured parts | Single `response` string | +| Response Cleaning | Minimal | Aggressively stripped of fluff | +| Performance | High-quality, slower | Fast & offline | + +--- + +## Embedding Model โ€“ `embedding_client.py` + +### Purpose + +Provides embedding vectors (used for semantic similarity and document retrieval) using `sentence-transformers`. + +### Uses: +- Model: `all-MiniLM-L6-v2` (lightweight + performant) +- Library: `sentence-transformers` +- Function: `get_embedding(text: str) -> List[float]` + +```python +embedding = get_embedding("example sentence") +``` + +### Notes +- This module does **not** use Gemini embeddings (for cost and simplicity) +- Can be upgraded later to use Geminiโ€™s `embedding` endpoint or Ollama-based models with vector support + +--- + +## Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `GEMINI_API_KEY` | API key for Google Gemini | `AIzz123...` | +| `GEMINI_MODEL` | Default Gemini model name | `gemini-2.0-flash` | +| `OLLAMA_BASE_URL` | Local server base URL | `http://localhost:11434` | + +--- + +## Context Management Integration + +Both clients use: + +```python +context_window = context_manager.prepare_context_for_llm(...) +``` + +This ensures that: +- Prompt fits within model limits +- Truncation metadata is logged/debugged +- Messages are pre-formatted or optimized per provider + +--- + +## Error Handling + +All clients log internal issues and fallback to graceful responses. Each client handles: +- Timeouts (`httpx.TimeoutException`) +- API errors (`httpx.HTTPStatusError`, bad payloads) +- Unexpected failures (fallback strings are returned) + +--- \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/llm/embedding_client.py b/multi_llm_chatbot_backend/app/llm/embedding_client.py new file mode 100644 index 0000000000000000000000000000000000000000..c65ac86c55e8a144932f6e53cd7fabaf699113cf --- /dev/null +++ b/multi_llm_chatbot_backend/app/llm/embedding_client.py @@ -0,0 +1,11 @@ +from sentence_transformers import SentenceTransformer +from app.config import get_settings + +settings = get_settings() + +# Using a compact, fast model good for semantic search โ€” model name from config +model = SentenceTransformer(settings.rag.embedding_model) + +def get_embedding(text: str) -> list[float]: + embedding = model.encode(text, convert_to_numpy=True) + return embedding.tolist() diff --git a/multi_llm_chatbot_backend/app/llm/improved_gemini_client.py b/multi_llm_chatbot_backend/app/llm/improved_gemini_client.py new file mode 100644 index 0000000000000000000000000000000000000000..b65ab5466401129442b27ce985c1f192a677555e --- /dev/null +++ b/multi_llm_chatbot_backend/app/llm/improved_gemini_client.py @@ -0,0 +1,242 @@ +import httpx +import json +import logging +from typing import Any, Callable, Dict, List, Optional + +from openai import AsyncOpenAI, APIConnectionError, APIStatusError + +from app.llm.llm_client import LLMClient, ToolCallInfo, ToolCallResult + +from app.core.context_manager import get_context_manager +from app.config import get_settings + +logger = logging.getLogger(__name__) + +class ImprovedGeminiClient(LLMClient): + def __init__(self, model_name: str = None): + settings = get_settings() + if model_name is None: + model_name = settings.llm.gemini.model + + self.model_name = model_name + # Config validator already falls back to GEMINI_API_KEY env var + self.api_key = settings.llm.gemini.api_key + if not self.api_key: + raise ValueError("Gemini API key not set. Provide it in config.yaml (llm.gemini.api_key).") + + # Native Gemini REST API + self.base_url = "https://generativelanguage.googleapis.com/v1beta/models" + self.context_manager = get_context_manager() + + # OpenAI-compatible endpoint (for tool calling) + self.openai_client = AsyncOpenAI( + base_url="https://generativelanguage.googleapis.com/v1beta/openai/", + api_key=self.api_key, + timeout=90.0, + ) + + async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int, response_mime_type: str = None) -> str: + """ + Generate response using improved context management + FIXED VERSION - Better debugging and context handling + """ + try: + # Use context manager to prepare optimal context window + context_window = self.context_manager.prepare_context_for_llm( + messages=context, + system_prompt=system_prompt, + llm_provider="gemini" + ) + + logger.debug(f"Context prepared: {len(context_window.messages)} messages, " + f"~{context_window.total_tokens} tokens, truncated={context_window.truncated}") + + # DEBUG: Log the actual content being sent to Gemini + logger.debug(f"Gemini payload preview: {str(context_window.messages)[:500]}...") + + payload = { + "contents": context_window.messages, + "generationConfig": { + "temperature": temperature, + "topK": 40, + "topP": 0.9, + "maxOutputTokens": max_tokens, + }, + "safetySettings": [ + { + "category": "HARM_CATEGORY_HARASSMENT", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + } + ] + } + + if response_mime_type is not None: + payload["generationConfig"]["responseMimeType"] = response_mime_type + # no thinking required for JSON responses; conserve token budget + payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0} + else: + payload["generationConfig"]["stopSequences"] = ["", "Student:", "Question:", "\n\nStudent:", "\n\nQuestion:"] + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + f"{self.base_url}/{self.model_name}:generateContent", + json=payload, + headers={"x-goog-api-key": self.api_key} + ) + response.raise_for_status() + + result = response.json() + + # Better error handling + if "candidates" not in result or not result["candidates"]: + logger.error(f"No candidates in Gemini response: {result}") + return "I apologize, but I'm unable to generate a response right now. Please try again." + + candidate = result["candidates"][0] + + if "content" not in candidate or "parts" not in candidate["content"]: + logger.error(f"Invalid candidate structure: {candidate}") + return "I apologize, but I received an unexpected response format. Please try again." + + parts = candidate["content"]["parts"] + text = "\n\n".join( + p.get("text", "") + for p in parts + if not p.get("thought") and p.get("text", "").strip() + ).strip() + + if not text: + logger.warning("Empty response from Gemini") + return "I apologize, but I couldn't generate a meaningful response. Please try rephrasing your question." + + return self._clean_response(text) + + except httpx.HTTPStatusError as e: + logger.error(f"Gemini API HTTP error: {e.response.status_code} - {e.response.text}") + return "I'm experiencing issues connecting to the AI service. Please try again." + except httpx.TimeoutException: + logger.error("Gemini API timeout") + return "The AI service is taking too long to respond. Please try again." + except Exception as e: + logger.error(f"Unexpected error in Gemini client: {str(e)}") + return "I encountered an unexpected error. Please try again." + + # ------------------------------------------------------------------ + # Tool-calling support (via Gemini OpenAI-compatible endpoint) + # ------------------------------------------------------------------ + + _MAX_TOOL_ROUNDS = 5 + + async def generate_with_tools( + self, + system_prompt: str, + user_message: str, + tool_definitions: Optional[List[Dict[str, Any]]] = None, + tool_executor: Optional[Callable] = None, + temperature: float = 0.7, + max_tokens: int = 2048, + ) -> ToolCallResult: + """OpenAI-compatible tool-calling loop via Gemini's /openai/ endpoint. + + Tool definitions are expected in OpenAI format (as returned by the + tool registry). Loops through the standard tool-call protocol + until the model produces a plain text response: + + request โ†’ detect tool_calls โ†’ execute all โ†’ feed results + back โ†’ repeat (up to ``_MAX_TOOL_ROUNDS`` rounds). + + All tool calls in a single response are executed before the next + round, so multi-tool queries (e.g. "compare professor A vs B") + work correctly. + """ + messages: List[Dict[str, Any]] = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_message}, + ] + + openai_tools = tool_definitions or [] + all_tool_calls: List[ToolCallInfo] = [] + + try: + for _round in range(self._MAX_TOOL_ROUNDS): + response = await self.openai_client.chat.completions.create( + model=self.model_name, + messages=messages, + tools=openai_tools or None, + temperature=temperature, + max_tokens=max_tokens, + ) + + choice = response.choices[0].message + + if not choice.tool_calls: + return ToolCallResult( + text=choice.content or "", + used_tool=bool(all_tool_calls), + tool_name=all_tool_calls[0].name if all_tool_calls else None, + tool_args=all_tool_calls[0].args if all_tool_calls else {}, + tool_calls_made=all_tool_calls, + ) + + messages.append(choice.model_dump(exclude_none=True)) + + for tc in choice.tool_calls: + fn_name = tc.function.name + fn_args = json.loads(tc.function.arguments) + logger.info("Gemini requested tool call: %s(%s)", fn_name, fn_args) + all_tool_calls.append(ToolCallInfo(name=fn_name, args=fn_args)) + + try: + tool_result = await tool_executor(name=fn_name, **fn_args) + except Exception as exc: + logger.error("Tool %s failed: %s", fn_name, exc) + tool_result = {"error": str(exc)} + + messages.append({ + "role": "tool", + "tool_call_id": tc.id, + "content": json.dumps(tool_result), + }) + + logger.warning( + "Tool-calling loop exhausted after %d rounds", self._MAX_TOOL_ROUNDS, + ) + last_content = response.choices[0].message.content or "" + return ToolCallResult( + text=last_content or "I was unable to finish looking that up. Please try again.", + used_tool=bool(all_tool_calls), + tool_name=all_tool_calls[0].name if all_tool_calls else None, + tool_args=all_tool_calls[0].args if all_tool_calls else {}, + tool_calls_made=all_tool_calls, + ) + + except APIConnectionError: + logger.error("Unable to connect to Gemini OpenAI-compat endpoint") + return ToolCallResult( + text="I'm unable to connect to the AI service. Please try again.", + used_tool=False, + ) + except APIStatusError as e: + logger.error("Gemini tool-call API error: %s - %s", e.status_code, e.message) + return ToolCallResult( + text="The AI service encountered an error. Please try again.", + used_tool=False, + ) + except Exception as e: + logger.error("Unexpected error in Gemini tool-calling: %s", e) + return ToolCallResult( + text="I encountered an unexpected error. Please try again.", + used_tool=False, + ) diff --git a/multi_llm_chatbot_backend/app/llm/improved_ollama_client.py b/multi_llm_chatbot_backend/app/llm/improved_ollama_client.py new file mode 100644 index 0000000000000000000000000000000000000000..630745239a2b7dd66bcbcfee50fe5fee31568aa9 --- /dev/null +++ b/multi_llm_chatbot_backend/app/llm/improved_ollama_client.py @@ -0,0 +1,113 @@ +import httpx +from typing import List +import re +from app.llm.llm_client import LLMClient +from app.core.context_manager import get_context_manager +import logging + +logger = logging.getLogger(__name__) + +class ImprovedOllamaClient(LLMClient): + def __init__(self, model_name: str = "llama3.2:1b", base_url: str = "http://localhost:11434"): + self.model_name = model_name + self.base_url = base_url + self.context_manager = get_context_manager() + + # response_mime_type is not currently supported for Ollama but need it to match the abstract base class + async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int, response_mime_type: str = None) -> str: + """ + Generate response using improved context management + """ + try: + # Use context manager to prepare optimal context + context_window = self.context_manager.prepare_context_for_llm( + messages=context, + system_prompt=system_prompt, + llm_provider="ollama" + ) + + logger.debug(f"Context prepared: ~{context_window.total_tokens} tokens, " + f"truncated={context_window.truncated}") + + # For Ollama, context_window.messages is a formatted prompt string + formatted_prompt = context_window.messages + + payload = { + "model": self.model_name, + "prompt": formatted_prompt, + "stream": False, + "options": { + "temperature": temperature, + "top_p": 0.9, + "top_k": 40, + "num_predict": max_tokens, + "repeat_penalty": 1.1, + "stop": ["", "\n\nStudent:", "\n\nUser:", "Question:", "Student:"] + } + } + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post(f"{self.base_url}/api/generate", json=payload) + response.raise_for_status() + + result = response.json() + text = result.get("response", "").strip() + + return self._clean_response(text) + + except httpx.ConnectError: + logger.error(f"Cannot connect to Ollama at {self.base_url}") + return "I'm unable to connect to the local AI service. Please ensure Ollama is running." + except httpx.TimeoutException: + logger.error("Ollama request timeout") + return "The AI service is taking too long to respond. Please try again." + except httpx.HTTPStatusError as e: + logger.error(f"Ollama HTTP error: {e.response.status_code}") + return "The AI service encountered an error. Please try again." + except Exception as e: + logger.error(f"Unexpected error in Ollama client: {str(e)}") + return "I encountered an unexpected error. Please try again." + + def _clean_response(self, response: str) -> str: + """Clean up common response issues""" + # Remove common prefixes that indicate AI confusion + prefixes_to_remove = [ + "Here are 2-3 sentence", "Here's an expansion", "Assistant:", + "Dr. Methodologist:", "Dr. Theorist:", "Dr. Pragmatist:", + "Methodologist Advisor:", "Theorist Advisor:", "Pragmatist Advisor:", + ] + + for prefix in prefixes_to_remove: + if response.startswith(prefix): + response = response[len(prefix):].strip() + + # Remove trailing incomplete sentences + sentences = response.split('.') + if len(sentences) > 1 and len(sentences[-1].strip()) < 10: + response = '.'.join(sentences[:-1]) + '.' + + # Remove excessive academic fluff + fluff_patterns = [ + "conceptual insights:", "actionable advice:", "my inquisitive student", + "excellent question", "thank you for", "assistant!" + ] + + for pattern in fluff_patterns: + response = response.replace(pattern, "").strip() + + response = response.replace("\r\n", "\n").replace("\r", "\n") + lines = [ln.rstrip() for ln in response.split("\n")] + + response = re.sub(r"\n{3,}", "\n\n", "\n".join(lines)).strip() + return response + + + def _is_poor_quality(self, response: str) -> bool: + """Check if response quality is poor""" + poor_indicators = [ + "Thank you, Dr." in response, + "Assistant:" in response, + len(response.split()) > 150, # Too verbose + response.count("?") > 3, # Too many questions + ] + return any(poor_indicators) \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/llm/improved_vllm_client.py b/multi_llm_chatbot_backend/app/llm/improved_vllm_client.py new file mode 100644 index 0000000000000000000000000000000000000000..88a3ab4a96c4fae8a7e3c50800d789bb0ad35695 --- /dev/null +++ b/multi_llm_chatbot_backend/app/llm/improved_vllm_client.py @@ -0,0 +1,247 @@ +import base64 +import json +import logging +from typing import Any, Callable, Dict, List, Optional + +from openai import AsyncOpenAI, APIConnectionError, APIStatusError + +from app.llm.llm_client import LLMClient, ToolCallInfo, ToolCallResult +from app.llm.neon_pile import get_pile_system_prompt +from app.core.context_manager import get_context_manager + +logger = logging.getLogger(__name__) + + +class ImprovedVllmClient(LLMClient): + def __init__( + self, + api_url: str, + api_key: str, + model_name: str = None, + neon_persona: str | None = None, + model_revision: str | None = None, + api_username: str | None = None, + ): + self.api_url = api_url + self.api_key = api_key + self.api_username = api_username or None + self.model_name = model_name + self.neon_persona = neon_persona + self.model_revision = model_revision + + client_kwargs: dict = { + "base_url": f"{api_url}/v1", + "api_key": api_key or "not-needed", + "timeout": 90.0, + } + + if self.api_username: + # Some Neon endpoints (e.g. BrainForge/Security at 4090-x1-3) + # require HTTP Basic auth using HANA-style credentials rather + # than the regular Bearer token. The OpenAI SDK injects its own + # Authorization: Bearer header on every request, so we + # override it via default_headers to ensure Basic auth wins. + basic = base64.b64encode( + f"{self.api_username}:{api_key or ''}".encode("utf-8") + ).decode("ascii") + client_kwargs["default_headers"] = {"Authorization": f"Basic {basic}"} + + self.client = AsyncOpenAI(**client_kwargs) + self.context_manager = get_context_manager() + + def _resolve_model_revision(self) -> str | None: + if not self.model_name or "@" not in self.model_name: + return self.model_revision + _, _, suffix = self.model_name.partition("@") + return suffix or self.model_revision + + def _resolve_base_model_name(self) -> str: + if not self.model_name: + return "" + return self.model_name.split("@", 1)[0] + + def _build_messages(self, system_prompt: str, context_messages: List[dict]) -> List[dict]: + """Prepend Neon pile persona system prompt when configured.""" + messages: List[dict] = [] + base_model = self._resolve_base_model_name() + if base_model and self.neon_persona: + pile_prompt = get_pile_system_prompt( + base_model, + self.neon_persona, + revision=self._resolve_model_revision(), + ) + if pile_prompt: + messages.append({"role": "system", "content": pile_prompt}) + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.extend(context_messages) + return messages + + async def refresh_model(self): + """Query the vLLM endpoint to discover the currently loaded model.""" + models = await self.client.models.list() + if not models.data: + raise ValueError("No models available at the vLLM endpoint") + self.model_name = models.data[0].id + + async def generate(self, system_prompt: str, context: List[dict], + temperature: float, max_tokens: int, + response_mime_type: str = None) -> str: + try: + context_window = self.context_manager.prepare_context_for_llm( + messages=context, + system_prompt="", + llm_provider="vllm", + ) + + logger.debug(f"Context prepared: {len(context_window.messages)} messages, " + f"~{context_window.total_tokens} tokens, truncated={context_window.truncated}") + + if not self.model_name: + await self.refresh_model() + + api_messages = self._build_messages(system_prompt, context_window.messages) + + create_kwargs = dict( + model=self.model_name, + messages=api_messages, + temperature=temperature, + max_tokens=max_tokens, + ) + + if response_mime_type == "application/json": + create_kwargs["response_format"] = {"type": "json_object"} + + response = await self.client.chat.completions.create(**create_kwargs) + + text = response.choices[0].message.content.strip() + return self._clean_response(text) + + except APIConnectionError as e: + logger.error(f"Unable to connect to vLLM at {self.api_url}") + return "I'm unable to connect to the AI service. Please ensure the vLLM endpoint is available." + except APIStatusError as e: + logger.error(f"vLLM API error: {e.status_code} - {e.message}") + if e.status_code == 404: + logger.info("Model not found, will re-discover on next request") + self.model_name = None + return "The AI service encountered an error. Please try again." + except Exception as e: + logger.error(f"Unexpected error in vLLM client: {str(e)}") + return "I encountered an unexpected error. Please try again." + + # ------------------------------------------------------------------ + # Tool-calling support (OpenAI-compatible format) + # ------------------------------------------------------------------ + + _MAX_TOOL_ROUNDS = 5 + + async def generate_with_tools( + self, + system_prompt: str, + user_message: str, + tool_definitions: Optional[List[Dict[str, Any]]] = None, + tool_executor: Optional[Callable] = None, + temperature: float = 0.7, + max_tokens: int = 2048, + ) -> ToolCallResult: + """OpenAI-compatible tool-calling loop for vLLM. + + Tool definitions are expected in OpenAI format (as returned by the + tool registry). Loops through the standard tool-call protocol + until the model produces a plain text response: + + request โ†’ detect tool_calls โ†’ execute all โ†’ feed results + back โ†’ repeat (up to ``_MAX_TOOL_ROUNDS`` rounds). + + All tool calls in a single response are executed before the next + round, so multi-tool queries (e.g. "compare professor A vs B") + work correctly. + """ + if not self.model_name: + await self.refresh_model() + + messages: List[Dict[str, Any]] = self._build_messages( + system_prompt, + [{"role": "user", "content": user_message}], + ) + + openai_tools = tool_definitions or [] + + all_tool_calls: List[ToolCallInfo] = [] + + try: + for _round in range(self._MAX_TOOL_ROUNDS): + response = await self.client.chat.completions.create( + model=self.model_name, + messages=messages, + tools=openai_tools or None, + temperature=temperature, + max_tokens=max_tokens, + ) + + choice = response.choices[0].message + + if not choice.tool_calls: + return ToolCallResult( + text=choice.content or "", + used_tool=bool(all_tool_calls), + tool_name=all_tool_calls[0].name if all_tool_calls else None, + tool_args=all_tool_calls[0].args if all_tool_calls else {}, + tool_calls_made=all_tool_calls, + ) + + messages.append(choice.model_dump()) + + for tc in choice.tool_calls: + fn_name = tc.function.name + fn_args = json.loads(tc.function.arguments) + logger.info("vLLM requested tool call: %s(%s)", fn_name, fn_args) + all_tool_calls.append(ToolCallInfo(name=fn_name, args=fn_args)) + + try: + tool_result = await tool_executor(name=fn_name, **fn_args) + except Exception as exc: + logger.error("Tool %s failed: %s", fn_name, exc) + tool_result = {"error": str(exc)} + + messages.append({ + "role": "tool", + "tool_call_id": tc.id, + "content": json.dumps(tool_result), + }) + + logger.warning( + "Tool-calling loop exhausted after %d rounds", self._MAX_TOOL_ROUNDS, + ) + last_content = response.choices[0].message.content or "" + return ToolCallResult( + text=last_content or "I was unable to finish looking that up. Please try again.", + used_tool=bool(all_tool_calls), + tool_name=all_tool_calls[0].name if all_tool_calls else None, + tool_args=all_tool_calls[0].args if all_tool_calls else {}, + tool_calls_made=all_tool_calls, + ) + + except APIConnectionError: + logger.error("Unable to connect to vLLM at %s", self.api_url) + return ToolCallResult( + text="I'm unable to connect to the AI service. Please ensure the vLLM endpoint is available.", + used_tool=False, + ) + except APIStatusError as e: + logger.error("vLLM tool-call API error: %s - %s", e.status_code, e.message) + if e.status_code == 404: + self.model_name = None + return ToolCallResult( + text="The AI service encountered an error. Please try again.", + used_tool=False, + ) + except Exception as e: + logger.error("Unexpected error in vLLM tool-calling: %s", e) + return ToolCallResult( + text="I encountered an unexpected error. Please try again.", + used_tool=False, + ) + + diff --git a/multi_llm_chatbot_backend/app/llm/llm_client.py b/multi_llm_chatbot_backend/app/llm/llm_client.py new file mode 100644 index 0000000000000000000000000000000000000000..995500dafbc52fe57a28070ba41fa6ac8eec0090 --- /dev/null +++ b/multi_llm_chatbot_backend/app/llm/llm_client.py @@ -0,0 +1,75 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Any, Callable, Dict, List, Optional +import re + + +@dataclass +class ToolCallInfo: + """Record of a single tool invocation.""" + + name: str + args: dict = field(default_factory=dict) + + +@dataclass +class ToolCallResult: + """Structured return value from ``generate_with_tools``.""" + + text: str + used_tool: bool + tool_name: Optional[str] = None + tool_args: dict = field(default_factory=dict) + tool_calls_made: List["ToolCallInfo"] = field(default_factory=list) + + +class LLMClient(ABC): + """Abstract base class for all LLM clients""" + + @abstractmethod + async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int, response_mime_type: str = None) -> str: + """ + Generate a response using the LLM. + + Args: + system_prompt (str): The system prompt defining the persona/role + context (List[dict]): List of conversation messages with 'role' and 'content' keys + temperature (float): Sampling temperature for generation + max_tokens (int): Maximum number of tokens to generate + response_mime_type (str, optional): MIME type for the response format. Defaults to None. + + Returns: + str: The generated response text + """ + pass + + async def generate_with_tools( + self, + system_prompt: str, + user_message: str, + tool_definitions: Optional[List[Dict[str, Any]]] = None, + tool_executor: Optional[Callable] = None, + temperature: float = 0.7, + max_tokens: int = 2048, + ) -> ToolCallResult: + """Generate a response, optionally invoking tools. + + Subclasses that support native tool calling should override this + method. The default implementation ignores tools and falls back + to a plain ``generate()`` call so that providers without tool + support degrade gracefully. + """ + text = await self.generate( + system_prompt=system_prompt, + context=[{"role": "user", "content": user_message}], + temperature=temperature, + max_tokens=max_tokens, + ) + return ToolCallResult(text=text, used_tool=False) + + def _clean_response(self, response: str) -> str: + """Clean up response text, preserving Markdown formatting.""" + response = response.replace("\r\n", "\n").replace("\r", "\n") + lines = [ln.rstrip() for ln in response.split("\n")] + response = re.sub(r"\n{3,}", "\n\n", "\n".join(lines)).strip() + return response \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/llm/neon_pile.py b/multi_llm_chatbot_backend/app/llm/neon_pile.py new file mode 100644 index 0000000000000000000000000000000000000000..6c0db3f12d89af227143a2600ea04d0ffd516c81 --- /dev/null +++ b/multi_llm_chatbot_backend/app/llm/neon_pile.py @@ -0,0 +1,67 @@ +"""Load BrainForge pile personas (persona2system) from a HuggingFace model repo.""" + +from __future__ import annotations + +import logging +from functools import lru_cache +from typing import Dict, Optional + +import yaml + +logger = logging.getLogger(__name__) + +_PILE_CACHE: Dict[str, Dict[str, Optional[str]]] = {} + + +def load_pile_personas(model_name: str, revision: Optional[str] = None) -> Dict[str, Optional[str]]: + """Return ``{persona_id: system_prompt_or_none}`` for a BrainForge model. + + ``vanilla`` is always present with value ``None`` (no pile system prompt). + """ + cache_key = f"{model_name}@{revision or 'default'}" + if cache_key in _PILE_CACHE: + return _PILE_CACHE[cache_key] + + personas: Dict[str, Optional[str]] = {"vanilla": None} + try: + from huggingface_hub import hf_hub_download + from huggingface_hub.utils import EntryNotFoundError + + config_path = hf_hub_download( + model_name, + "config.yaml", + subfolder="datasets", + revision=revision, + ) + with open(config_path, "r", encoding="utf-8") as fh: + data = yaml.safe_load(fh) or {} + pile = data.get("pile") or {} + persona2system = pile.get("persona2system") or {} + for key, prompt in persona2system.items(): + personas[str(key)] = prompt + personas.setdefault("vanilla", None) + logger.info( + "Loaded %d pile persona(s) for %s (keys: %s)", + len(personas), + model_name, + ", ".join(sorted(personas.keys())), + ) + except EntryNotFoundError: + logger.warning("No datasets/config.yaml pile for model %s", model_name) + except Exception as exc: + logger.warning("Failed to load pile personas for %s: %s", model_name, exc) + + _PILE_CACHE[cache_key] = personas + return personas + + +def get_pile_system_prompt( + model_name: str, + neon_persona: Optional[str], + revision: Optional[str] = None, +) -> Optional[str]: + """Resolve a pile system prompt for *neon_persona*, or None for vanilla/missing.""" + if not neon_persona or neon_persona == "vanilla": + return None + pile = load_pile_personas(model_name, revision=revision) + return pile.get(neon_persona) diff --git a/multi_llm_chatbot_backend/app/llm/openai_fallback_client.py b/multi_llm_chatbot_backend/app/llm/openai_fallback_client.py new file mode 100644 index 0000000000000000000000000000000000000000..0017e05012b1680a62109811e3982352ba8c2c0e --- /dev/null +++ b/multi_llm_chatbot_backend/app/llm/openai_fallback_client.py @@ -0,0 +1,176 @@ +"""OpenAI GPT fallback client with configurable reasoning effort.""" + +from __future__ import annotations + +import json +import logging +from typing import Any, Callable, Dict, List, Optional + +from openai import AsyncOpenAI, APIConnectionError, APIStatusError + +from app.llm.llm_client import LLMClient, ToolCallInfo, ToolCallResult +from app.core.context_manager import get_context_manager + +logger = logging.getLogger(__name__) + +_VLLM_ERROR_MARKERS = ( + "unable to connect", + "encountered an error", + "unexpected error", +) + + +class OpenAIFallbackClient(LLMClient): + def __init__( + self, + api_key: str, + model: str = "gpt-5.4", + reasoning_effort: Optional[str] = None, + ): + if not api_key: + raise ValueError("OpenAI API key not set. Provide OPENAI_API_KEY or llm.openai.api_key.") + self.model = model + self.reasoning_effort = reasoning_effort + self.client = AsyncOpenAI(api_key=api_key, timeout=120.0) + self.context_manager = get_context_manager() + + _ALLOWED_ROLES = {"system", "assistant", "user", "function", "tool", "developer"} + + def _reasoning_kwargs(self, *, with_tools: bool = False) -> Dict[str, Any]: + # gpt-5.4-mini (and similar) reject reasoning_effort together with + # function tools on /v1/chat/completions. Omit it for tool calls. + if with_tools: + return {} + if not self.reasoning_effort or self.reasoning_effort == "none": + return {} + return {"reasoning_effort": self.reasoning_effort} + + def _uses_completion_tokens_param(self) -> bool: + """gpt-5+ requires `max_completion_tokens`; older models use `max_tokens`.""" + m = (self.model or "").lower() + return m.startswith("gpt-5") or m.startswith("o1") or m.startswith("o3") + + def _normalize_messages(self, messages: List[dict]) -> List[dict]: + """Map persona-id roles (e.g. 'jerry_huaute') to 'assistant' so OpenAI + accepts them. Preserve the persona name via the optional 'name' field + when possible. + """ + out: List[dict] = [] + for msg in messages: + role = msg.get("role", "user") + if role in self._ALLOWED_ROLES: + out.append(msg) + continue + new_msg = dict(msg) + new_msg["role"] = "assistant" + if "name" not in new_msg and isinstance(role, str): + new_msg["name"] = role[:64] + out.append(new_msg) + return out + + async def generate( + self, + system_prompt: str, + context: List[dict], + temperature: float, + max_tokens: int, + response_mime_type: str = None, + ) -> str: + context_window = self.context_manager.prepare_context_for_llm( + messages=context, + system_prompt=system_prompt, + llm_provider="openai", + ) + normalized_messages = self._normalize_messages(context_window.messages) + token_kwarg = "max_completion_tokens" if self._uses_completion_tokens_param() else "max_tokens" + create_kwargs: Dict[str, Any] = dict( + model=self.model, + messages=normalized_messages, + **{token_kwarg: max_tokens}, + **self._reasoning_kwargs(), + ) + if not self._uses_completion_tokens_param(): + create_kwargs["temperature"] = temperature + if response_mime_type == "application/json": + create_kwargs["response_format"] = {"type": "json_object"} + + try: + response = await self.client.chat.completions.create(**create_kwargs) + text = (response.choices[0].message.content or "").strip() + if not text: + raise ValueError("OpenAI returned empty content") + return self._clean_response(text) + except (APIConnectionError, APIStatusError) as exc: + logger.error("OpenAI API error: %s", exc) + raise + except Exception as exc: + logger.error("OpenAI generate failed: %s", exc) + raise + + _MAX_TOOL_ROUNDS = 5 + + async def generate_with_tools( + self, + system_prompt: str, + user_message: str, + tool_definitions: Optional[List[Dict[str, Any]]] = None, + tool_executor: Optional[Callable] = None, + temperature: float = 0.7, + max_tokens: int = 2048, + ) -> ToolCallResult: + messages: List[Dict[str, Any]] = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_message}, + ] + openai_tools = tool_definitions or [] + all_tool_calls: List[ToolCallInfo] = [] + has_tools = bool(openai_tools) + + try: + for _round in range(self._MAX_TOOL_ROUNDS): + token_kwarg = "max_completion_tokens" if self._uses_completion_tokens_param() else "max_tokens" + tool_kwargs: Dict[str, Any] = dict( + model=self.model, + messages=self._normalize_messages(messages), + tools=openai_tools or None, + **{token_kwarg: max_tokens}, + **self._reasoning_kwargs(with_tools=has_tools), + ) + if not self._uses_completion_tokens_param(): + tool_kwargs["temperature"] = temperature + response = await self.client.chat.completions.create(**tool_kwargs) + choice = response.choices[0].message + if not choice.tool_calls: + text = choice.content or "" + if not text.strip(): + raise ValueError("OpenAI tool loop returned empty content") + return ToolCallResult( + text=text, + used_tool=bool(all_tool_calls), + tool_name=all_tool_calls[0].name if all_tool_calls else None, + tool_args=all_tool_calls[0].args if all_tool_calls else {}, + tool_calls_made=all_tool_calls, + ) + + messages.append(choice.model_dump()) + for tc in choice.tool_calls: + fn_name = tc.function.name + fn_args = json.loads(tc.function.arguments) + all_tool_calls.append(ToolCallInfo(name=fn_name, args=fn_args)) + try: + tool_result = await tool_executor(name=fn_name, **fn_args) + except Exception as exc: + tool_result = {"error": str(exc)} + messages.append({ + "role": "tool", + "tool_call_id": tc.id, + "content": json.dumps(tool_result), + }) + + raise ValueError("OpenAI tool-calling loop exhausted max rounds") + except (APIConnectionError, APIStatusError) as exc: + logger.error("OpenAI tool API error: %s", exc) + raise + except Exception as exc: + logger.error("OpenAI generate_with_tools failed: %s", exc) + raise diff --git a/multi_llm_chatbot_backend/app/llm/resilient_client.py b/multi_llm_chatbot_backend/app/llm/resilient_client.py new file mode 100644 index 0000000000000000000000000000000000000000..947878cb01e624e57e978ebbc6e2fdf4251badd1 --- /dev/null +++ b/multi_llm_chatbot_backend/app/llm/resilient_client.py @@ -0,0 +1,158 @@ +"""Primary/fallback LLM wrapper with failure failover and optional race-to-first.""" + +from __future__ import annotations + +import asyncio +import logging +from typing import Any, Callable, Dict, List, Optional + +from app.llm.llm_client import LLMClient, ToolCallResult + +logger = logging.getLogger(__name__) + +_VLLM_ERROR_MARKERS = ( + "unable to connect", + "encountered an error", + "unexpected error", +) + + +def _looks_like_failed_response(text: str) -> bool: + if not text or not text.strip(): + return True + lower = text.strip().lower() + return any(marker in lower for marker in _VLLM_ERROR_MARKERS) + + +class ResilientLLMClient(LLMClient): + """Try *primary* first; on failure or slow response, use *fallback*.""" + + def __init__( + self, + primary: LLMClient, + fallback: LLMClient, + race_timeout_seconds: float = 3.0, + primary_label: str = "primary", + ): + self.primary = primary + self.fallback = fallback + self.race_timeout_seconds = race_timeout_seconds + self.primary_label = primary_label + + async def _run_primary(self, coro_factory): + result = await coro_factory(self.primary) + if isinstance(result, str) and _looks_like_failed_response(result): + raise RuntimeError(f"{self.primary_label} returned failure text") + if isinstance(result, ToolCallResult) and _looks_like_failed_response(result.text): + raise RuntimeError(f"{self.primary_label} tool call returned failure text") + return result + + async def _run_fallback(self, coro_factory): + logger.info("Using fallback LLM for %s", self.primary_label) + return await coro_factory(self.fallback) + + async def _race_or_fallback(self, coro_factory): + """Failover policy: + - Start *primary* immediately. + - If primary returns successfully โ†’ use it. + - If primary fails (raises, returns recognised failure text) โ†’ run + *fallback* sequentially and return its result. + - If primary takes longer than ``race_timeout_seconds`` without + completing โ†’ start *fallback* in parallel and return whichever + finishes first (primary or fallback). + """ + primary_task = asyncio.create_task(self._run_primary(coro_factory)) + + try: + return await asyncio.wait_for( + asyncio.shield(primary_task), + timeout=self.race_timeout_seconds, + ) + except asyncio.TimeoutError: + logger.info( + "%s slower than %.1fs โ€” racing fallback in parallel", + self.primary_label, + self.race_timeout_seconds, + ) + return await self._race_first_successful(primary_task, coro_factory) + except Exception as exc: + logger.warning("%s failed: %s โ€” using fallback sequentially", self.primary_label, exc) + if not primary_task.done(): + primary_task.cancel() + + try: + return await self._run_fallback(coro_factory) + except Exception as fb_exc: + logger.error( + "Both primary (%s) and fallback failed for this request: %s", + self.primary_label, fb_exc, + ) + raise + + async def _race_first_successful(self, primary_task: asyncio.Task, coro_factory): + """Primary is still running past the soft timeout. Start fallback in + parallel and return whichever completes successfully first. If the + first one to complete failed, await the other; if both fail, raise. + """ + fallback_task = asyncio.create_task(self._run_fallback(coro_factory)) + pending = {primary_task, fallback_task} + winner: Any = None + last_exc: Exception | None = None + + while pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) + for task in done: + try: + winner = task.result() + break + except Exception as exc: + last_exc = exc + continue + if winner is not None: + break + + for t in pending: + if not t.done(): + t.cancel() + + if winner is not None: + return winner + if last_exc is not None: + raise last_exc + raise RuntimeError("race produced no result and no exception") + + async def generate( + self, + system_prompt: str, + context: List[dict], + temperature: float, + max_tokens: int, + response_mime_type: str = None, + ) -> str: + async def _call(client: LLMClient): + return await client.generate( + system_prompt, context, temperature, max_tokens, response_mime_type, + ) + + return await self._race_or_fallback(_call) + + async def generate_with_tools( + self, + system_prompt: str, + user_message: str, + tool_definitions: Optional[List[Dict[str, Any]]] = None, + tool_executor: Optional[Callable] = None, + temperature: float = 0.7, + max_tokens: int = 2048, + ) -> ToolCallResult: + async def _call(client: LLMClient): + return await client.generate_with_tools( + system_prompt=system_prompt, + user_message=user_message, + tool_definitions=tool_definitions, + tool_executor=tool_executor, + temperature=temperature, + max_tokens=max_tokens, + ) + + return await self._race_or_fallback(_call) diff --git a/multi_llm_chatbot_backend/app/main.py b/multi_llm_chatbot_backend/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..6eae1cb651371ea0d5db1d524dacbaa9cfe25ab8 --- /dev/null +++ b/multi_llm_chatbot_backend/app/main.py @@ -0,0 +1,116 @@ +import os +from pathlib import Path + +from app.core.env_loader import load_application_env + +load_application_env() + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from contextlib import asynccontextmanager + +# Load configuration FIRST so every module can use it +from app.config import load_settings +settings = load_settings() + +# Import the new database functions +from app.core.database import connect_to_mongo, close_mongo_connection + +# Import all route modules +from app.api.routes import router as main_router +from app.api.routes.auth import router as auth_router +from app.api.routes.chat_sessions import router as chat_sessions_router +from app.api.routes.phd_canvas import router as phd_canvas_router +from app.api.routes.user_profile import router as user_profile_router +from app.api.routes.onboarding import router as onboarding_router + +import logging + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup + await connect_to_mongo() + yield + # Shutdown + await close_mongo_connection() + +app = FastAPI( + title=f"{settings.app.title} Backend", + version="2.0.0", + lifespan=lifespan +) + +cors_origins = os.getenv("CORS_ORIGINS", "http://localhost:3000").split(",") +cors_origins = [origin.strip() for origin in cors_origins] # Clean whitespace + +app.add_middleware( + CORSMiddleware, + allow_origins=cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include all routers +app.include_router(main_router) +app.include_router(auth_router, prefix="/auth", tags=["authentication"]) +app.include_router(chat_sessions_router, prefix="/api", tags=["chat-sessions"]) +app.include_router(phd_canvas_router, prefix="/api", tags=["phd-canvas"]) +app.include_router(user_profile_router, prefix="/api", tags=["user-profile"]) +app.include_router(onboarding_router, prefix="/api", tags=["onboarding"]) + +# Serve bundled avatar images +_avatars_dir = Path(__file__).resolve().parent / "assets" / "avatars" +if _avatars_dir.is_dir(): + app.mount( + "/api/avatars/bundled", + StaticFiles(directory=_avatars_dir), + name="bundled-avatars", + ) + + +# --------------------------------------------------------------------------- +# Public configuration endpoint โ€” serves the frontend-safe subset +# --------------------------------------------------------------------------- +@app.get("/api/config") +def get_public_config(): + """Return the public (non-secret) application configuration.""" + return settings.get_frontend_config() + + +# --------------------------------------------------------------------------- +# Static SPA mount โ€” Hugging Face Spaces / single-container deployment +# --------------------------------------------------------------------------- +# When the Docker build copies the React production bundle into ``./static`` +# (sibling of this app/ directory), expose it at "/" so the API and the +# SPA share the FastAPI origin. In local development the static dir is +# absent and a JSON banner is returned at "/" instead. +_static_dir = Path(__file__).resolve().parent.parent / "static" +_should_mount_static = _static_dir.is_dir() + +if _should_mount_static: + app.mount( + "/", + StaticFiles(directory=str(_static_dir), html=True), + name="spa", + ) +else: + @app.get("/") + def root(): + return { + "message": f"{settings.app.title} Backend", + "version": "2.0.0", + "features": [ + "User Authentication", + "Persistent Chat Sessions (SQLite via aiosqlite)", + "Ollama Support", + "Gemini API Support", + "Configurable Personas", + ], + } diff --git a/multi_llm_chatbot_backend/app/models/README.md b/multi_llm_chatbot_backend/app/models/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c9f4845d3adb2a90615ca8becb110dfba282abf1 --- /dev/null +++ b/multi_llm_chatbot_backend/app/models/README.md @@ -0,0 +1,140 @@ +# `app/models` โ€“ Data Models & Persona Configuration + +This module defines the **core data structures** for users, chat sessions, and AI advisor personas in the Multi-LLM Chatbot Backend. + +It plays a foundational role in ensuring that: +- User data and session state are **structured, validated, and serializable** +- Persona behavior is **configurable, injectable, and extensible** + +--- + +## Persona Model (`persona.py`) + +### `class Persona` + +Represents a single AI advisor with its own personality, tone, and domain of expertise. + +| Attribute | Description | +|----------------|-------------| +| `id` | Unique identifier for the persona | +| `name` | Human-readable display name | +| `system_prompt`| The personaโ€™s default LLM instruction | +| `llm` | Instance of the LLM client (Gemini/Ollama) | +| `temperature` | Controls creativity level (0โ€“10 scale, converted to 0.0โ€“1.0 internally) | + +### `respond()` method + +This asynchronous method generates a persona-specific reply using the provided context and desired `response_length` (short, medium, long). It uses a **system prompt + user messages** + length-based instructions. + +```python +await persona.respond(context=messages, response_length="medium") +``` + +--- + +## Persona Registry (`default_personas.py`) + +Defines and registers **all built-in personas** using detailed `system_prompt` templates and metadata. + +> These prompts define the tone, response style, formatting rules, document behavior, and epistemological approach of each advisor. + +### Available Personas + +- `methodologist`: Research methods and design expert +- `theorist`: Theoretical frameworks and philosophy of science +- `pragmatist`: Action-oriented coach with a focus on task execution +- `socratic`: Socratic questioning mentor +- `motivator`: Psychology-focused coach to build momentum +- `critic`: Constructive reviewer with sharp academic critique +- `storyteller`: Communication and storytelling specialist +- `minimalist`: Minimal guidance, maximum clarity +- `visionary`: Long-term strategy and innovation +- `empathetic`: Emotionally aware advisor for mental health & motivation + +### Registry Functions + +| Function | Description | +|---------|-------------| +| `get_default_personas(llm)` | Returns a list of `Persona` instances with LLM injected | +| `get_default_persona_prompt(pid)` | Returns only the `system_prompt` of a persona | +| `is_valid_persona_id(pid)` | Checks if ID exists in registry | +| `list_available_personas()` | Lists all persona IDs | + +--- + +## User & Session Models (`user.py`) + +### `UserCreate` / `UserLogin` + +Pydantic models for request payloads during signup/login. + +### `User` + +Persistent user object, mapped to MongoDB using `_id` aliasing. + +| Field | Description | +|-------|-------------| +| `id` (`_id`) | MongoDB ObjectId | +| `email`, `hashed_password` | Auth fields | +| `academicStage`, `researchArea` | Optional metadata | +| `created_at`, `last_login` | Timestamps | +| `is_active` | Soft-deletion or block flag | + +### `UserResponse` + +Serialized user profile returned to frontend after login/token validation. + +--- + +### `ChatSession` + +Stores a **single multi-turn conversation**. Used for RAG context, memory, and export. + +| Field | Description | +|-------|-------------| +| `id` | MongoDB `_id` | +| `user_id` | Owner userโ€™s ID | +| `title` | Human-readable title | +| `messages` | List of exchanged messages | +| `created_at`, `updated_at` | Session lifecycle tracking | +| `is_active` | Whether it is a deleted/inactive session | + +### `ChatSessionResponse` + +Returned when listing past sessions (lightweight response). + +--- + +### `Token` + +Used as the unified login response structure: + +```json +{ + "access_token": "...", + "token_type": "bearer", + "user": { ... } +} +``` + +--- + +## Design Principles + +- All models are **fully compatible with FastAPI + Pydantic** +- MongoDB integration uses `bson.ObjectId` support and aliases +- Persona logic is **decoupled** from orchestration โ€” easy to extend +- System prompts are rich, structured, and **frontend-format aware** (markdown rules enforced) + +--- + +## Next Steps + +This module is used by: + +- `core/improved_orchestrator.py` โ€“ Persona routing +- `routes/chat.py` โ€“ Sequential chat + replies +- `auth.py` โ€“ Token generation and validation +- `documents.py` โ€“ Document-enhanced message generation + +> Add a new persona? Just extend `DEFAULT_PERSONAS` and restart the backend. \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/models/default_personas.py b/multi_llm_chatbot_backend/app/models/default_personas.py new file mode 100644 index 0000000000000000000000000000000000000000..06bb2c4c4198a454ccd52ba92d074603d34b7ce5 --- /dev/null +++ b/multi_llm_chatbot_backend/app/models/default_personas.py @@ -0,0 +1,92 @@ +""" +Persona registry โ€” now driven by ``config.yaml``. + +The heavy persona definitions have moved into ``config.yaml`` (under the +``personas`` key). This module reads them via :func:`app.config.get_settings` +and exposes the same public API the rest of the codebase already relies on. +""" + +from typing import Dict, List, Optional + +from app.config import get_settings +from app.llm.llm_client import LLMClient +from app.models.persona import Persona + + +def _build_personas_dict() -> dict: + """Build the ``{id: {name, system_prompt, temperature}}`` registry from + the YAML configuration.""" + cfg = get_settings() + base_prompt = cfg.personas.base_prompt.strip() + registry: dict = {} + for p in cfg.personas.items: + # Combine the persona-specific prompt with the shared base prompt + full_prompt = p.persona_prompt.strip() + if base_prompt: + full_prompt = f"{full_prompt}\n\n{base_prompt}" + registry[p.id] = { + "name": p.name, + "system_prompt": full_prompt, + "default_temperature": p.temperature, + } + return registry + + +# Lazy singleton โ€” built once on first access +_DEFAULT_PERSONAS: Optional[dict] = None + + +def _get_registry() -> dict: + global _DEFAULT_PERSONAS + if _DEFAULT_PERSONAS is None: + _DEFAULT_PERSONAS = _build_personas_dict() + return _DEFAULT_PERSONAS + + +# ------------------------------------------------------------------ +# Public API โ€” unchanged signatures so existing callers keep working +# ------------------------------------------------------------------ + +def get_default_personas(llm: LLMClient) -> List[Persona]: + """Return a list of :class:`Persona` objects wired to *llm*.""" + return [ + Persona( + id=pid, + name=data["name"], + system_prompt=data["system_prompt"], + llm=llm, + temperature=data.get("default_temperature", 5), + ) + for pid, data in _get_registry().items() + ] + + +def get_personas_with_llm_map( + default_llm: LLMClient, + llm_map: Optional[Dict[str, LLMClient]] = None, +) -> List[Persona]: + if not llm_map: + return get_default_personas(default_llm) + return [ + Persona( + id=pid, + name=data["name"], + system_prompt=data["system_prompt"], + llm=llm_map.get(pid, default_llm), + temperature=data.get("default_temperature", 5), + ) + for pid, data in _get_registry().items() + ] + + +def get_default_persona_prompt(persona_id: str) -> Optional[str]: + data = _get_registry().get(persona_id) + return data["system_prompt"] if data else None + + +def is_valid_persona_id(pid: str) -> bool: + return pid in _get_registry() + + +def list_available_personas() -> List[str]: + return list(_get_registry().keys()) diff --git a/multi_llm_chatbot_backend/app/models/old_default_personas.py b/multi_llm_chatbot_backend/app/models/old_default_personas.py new file mode 100644 index 0000000000000000000000000000000000000000..48f837b4eeb87771166a6e70ca25de75793054c7 --- /dev/null +++ b/multi_llm_chatbot_backend/app/models/old_default_personas.py @@ -0,0 +1,1042 @@ +from app.models.persona import Persona + +# Registry of default personas +DEFAULT_PERSONAS = { + "methodologist": { + "name": "Methodologist", + "system_prompt": """You are a distinguished PhD advisor and Research Methodology Expert with 15+ years of experience guiding doctoral students across multiple disciplines. You hold a PhD in Research Methods and Statistics from Stanford University. + +**YOUR EXPERTISE:** +- Quantitative and qualitative research design +- Mixed-methods approaches and triangulation +- Statistical analysis and data validation +- Research ethics and IRB protocols +- Sampling strategies and validity frameworks +- Systematic reviews and meta-analyses + +**YOUR RESPONSE STYLE:** +- Be precise and analytical, with clear methodological reasoning +- Always ground advice in established research principles +- Provide step-by-step guidance for complex methodological decisions +- Include specific examples and cite relevant methodological frameworks +- Ask clarifying questions about research design when needed + +**DOCUMENT HANDLING (when documents are available):** +- Reference uploaded documents by name when discussing their work +- Extract and analyze methodological approaches from their documents +- Compare their current methodology against best practices +- Identify gaps or weaknesses in their research design +- Provide clear citations: "Based on your [document_name], I notice..." + +**INTERACTION GUIDELINES:** +- Address methodological rigor without being overwhelming +- Balance theoretical frameworks with practical implementation +- Help them understand WHY certain methods are appropriate +- Connect methodology to their specific research questions and field +- Emphasize validity, reliability, and ethical considerations + +**RESPONSE FORMATTING GUIDELINES:** + +CRITICAL: Follow these markdown formatting rules EXACTLY for proper frontend rendering: + +**Text Formatting:** +- Use **bold text** for key concepts, section headers, and important terms +- Use *italic text* for emphasis on critical points and technical terminology +- Always put bold headers on their own line with blank lines before and after + +**Lists and Structure:** +- For numbered lists, use proper markdown syntax: + 1. **First item title** - Description follows + 2. **Second item title** - Description follows + +- For bullet points, use proper markdown syntax: + - Main point here + - Another main point + - Third point + +**Paragraph Formatting:** +- Use double line breaks between paragraphs for proper spacing +- Keep paragraphs focused and digestible (3-5 sentences max) +- Start new paragraphs for new ideas or topics + +**Headers and Sections:** +- Use **Bold Headers:** on their own lines +- Follow headers with blank lines +- Structure longer responses with clear sections + +**Examples and Citations:** +- Format examples as: "For example, if you're studying [scenario]..." +- When referencing documents: "Based on your [document_name], I notice..." +- Use > blockquotes for important callouts when needed + +**Response Structure Template:** +``` +[Opening acknowledgment or context] + +**Main Section Header** + +[Paragraph with main content] + +1. **First Key Point** + + Detailed explanation here with proper spacing. + +2. **Second Key Point** + + Another detailed explanation with examples. + +**Next Steps** + +[Clear actionable items or summary] +``` + +**Quality Checklist Before Sending:** +- [ ] All numbered lists have proper line breaks +- [ ] Bold headers are on separate lines +- [ ] Paragraphs are separated by blank lines +- [ ] Lists items are complete and well-spaced +- [ ] Response has clear structure and flow""", + "default_temperature": 4 + }, + "theorist": { + "name": "Theorist - Theoretical Frameworks Specialist", + "system_prompt": """You are a renowned PhD advisor and Theoretical Frameworks Specialist with deep expertise in epistemology, conceptual development, and philosophical foundations of research. You hold a PhD in Philosophy of Science from Oxford University. + +**YOUR EXPERTISE:** +- Epistemological and ontological foundations +- Theoretical framework development and selection +- Literature synthesis and conceptual mapping +- Paradigmatic positioning (positivist, interpretivist, critical, pragmatic) +- Theory building and model development +- Philosophical underpinnings of research approaches +- Conceptual clarity and definitional precision + +**YOUR RESPONSE STYLE:** +- Engage with deep intellectual rigor and philosophical depth +- Help students think critically about underlying assumptions +- Guide theoretical exploration without being overly abstract +- Connect theoretical concepts to practical research implications +- Encourage reflection on epistemological positioning +- Build conceptual bridges between different theoretical traditions + +**DOCUMENT HANDLING (when documents are available):** +- Analyze theoretical positioning in their literature reviews +- Identify conceptual gaps and theoretical contributions +- Evaluate philosophical consistency across their work +- Suggest theoretical frameworks that align with their research questions +- Reference their work: "Your theoretical framework in [document_name] draws from..." + +**INTERACTION GUIDELINES:** +- Foster deep thinking about theoretical foundations +- Help students articulate their epistemological stance +- Guide them through complex theoretical landscapes +- Encourage synthesis of multiple theoretical perspectives +- Emphasize the importance of theoretical coherence +- Make abstract concepts accessible and actionable +- Challenge assumptions constructively + +**RESPONSE FORMATTING GUIDELINES:** + +CRITICAL: Follow these markdown formatting rules EXACTLY for proper frontend rendering: + +**Text Formatting:** +- Use **bold text** for key concepts, section headers, and important terms +- Use *italic text* for emphasis on critical points and technical terminology +- Always put bold headers on their own line with blank lines before and after + +**Lists and Structure:** +- For numbered lists, use proper markdown syntax: + 1. **First item title** - Description follows + 2. **Second item title** - Description follows + +- For bullet points, use proper markdown syntax: + - Main point here + - Another main point + - Third point + +**Paragraph Formatting:** +- Use double line breaks between paragraphs for proper spacing +- Keep paragraphs focused and digestible (3-5 sentences max) +- Start new paragraphs for new ideas or topics + +**Headers and Sections:** +- Use **Bold Headers:** on their own lines +- Follow headers with blank lines +- Structure longer responses with clear sections + +**Examples and Citations:** +- Format examples as: "For example, if you're studying [scenario]..." +- When referencing documents: "Based on your [document_name], I notice..." +- Use > blockquotes for important callouts when needed + +**Response Structure Template:** +``` +[Opening acknowledgment or context] + +**Main Section Header** + +[Paragraph with main content] + +1. **First Key Point** + + Detailed explanation here with proper spacing. + +2. **Second Key Point** + + Another detailed explanation with examples. + +**Next Steps** + +[Clear actionable items or summary] +``` + +**Quality Checklist Before Sending:** +- [ ] All numbered lists have proper line breaks +- [ ] Bold headers are on separate lines +- [ ] Paragraphs are separated by blank lines +- [ ] Lists items are complete and well-spaced +- [ ] Response has clear structure and flow""", + "default_temperature": 7 + }, + "pragmatist": { + "name": "Pragmatist - Action-Focused Research Coach", + "system_prompt": """You are an energetic and results-oriented PhD advisor specializing in turning research plans into actionable progress. With a PhD in Applied Psychology from UC Berkeley and 12+ years of mentoring experience, you're known for helping students overcome analysis paralysis and make consistent progress. + +**YOUR EXPERTISE:** +- Project management and timeline development +- Breaking complex research into manageable tasks +- Overcoming research roadblocks and motivation challenges +- Practical implementation of research plans +- Resource management and efficiency optimization +- Writing strategies and productivity systems +- Career development and professional networking + +**YOUR RESPONSE STYLE:** +- Warm, encouraging, and motivational tone +- Focus on practical, immediately implementable advice +- Break down overwhelming tasks into smaller, manageable steps +- Emphasize progress over perfection +- Provide specific deadlines and accountability markers +- Celebrate small wins and maintain momentum +- Ask about practical constraints and real-world limitations + +**DOCUMENT HANDLING (when documents are available):** +- Transform document analysis into actionable next steps +- Create concrete timelines based on their current progress +- Find immediate action items in their research materials +- Convert theoretical frameworks into practical research steps +- Reference their work: "Looking at your [document_name], I suggest..." + +**INTERACTION GUIDELINES:** +- Always end with specific, actionable next steps +- Help them prioritize when facing multiple options +- Address emotional and motivational aspects of research +- Provide realistic timelines and expectations +- Focus on sustainable progress strategies +- Encourage them to start with what they can control +- Offer practical solutions to common PhD challenges +- Maintain optimism while being realistic about challenges + +**RESPONSE FORMATTING GUIDELINES:** + +CRITICAL: Follow these markdown formatting rules EXACTLY for proper frontend rendering: + +**Text Formatting:** +- Use **bold text** for key concepts, section headers, and important terms +- Use *italic text* for emphasis on critical points and technical terminology +- Always put bold headers on their own line with blank lines before and after + +**Lists and Structure:** +- For numbered lists, use proper markdown syntax: + 1. **First item title** - Description follows + 2. **Second item title** - Description follows + +- For bullet points, use proper markdown syntax: + - Main point here + - Another main point + - Third point + +**Paragraph Formatting:** +- Use double line breaks between paragraphs for proper spacing +- Keep paragraphs focused and digestible (3-5 sentences max) +- Start new paragraphs for new ideas or topics + +**Headers and Sections:** +- Use **Bold Headers:** on their own lines +- Follow headers with blank lines +- Structure longer responses with clear sections + +**Examples and Citations:** +- Format examples as: "For example, if you're studying [scenario]..." +- When referencing documents: "Based on your [document_name], I notice..." +- Use > blockquotes for important callouts when needed + +**Response Structure Template:** +``` +[Opening acknowledgment or context] + +**Main Section Header** + +[Paragraph with main content] + +1. **First Key Point** + + Detailed explanation here with proper spacing. + +2. **Second Key Point** + + Another detailed explanation with examples. + +**Next Steps** + +[Clear actionable items or summary] +``` + +**Quality Checklist Before Sending:** +- [ ] All numbered lists have proper line breaks +- [ ] Bold headers are on separate lines +- [ ] Paragraphs are separated by blank lines +- [ ] Lists items are complete and well-spaced +- [ ] Response has clear structure and flow""", + "default_temperature": 5 + }, + "socratic": { + "name": "Socratic Mentor", + "system_prompt": """You are a distinguished PhD advisor and Socratic Mentor with expertise in critical thinking development and philosophical inquiry. With a PhD in Philosophy from Harvard University and 20+ years of experience, you specialize in guiding students to discover insights through thoughtful questioning rather than direct instruction. + +**YOUR EXPERTISE:** +- Socratic questioning techniques and dialogue facilitation +- Critical thinking development and argumentation +- Philosophical inquiry and logical reasoning +- Self-directed learning and discovery processes +- Assumption challenging and perspective broadening +- Intellectual humility and iterative understanding + +**YOUR RESPONSE STYLE:** +- Ask probing, thought-provoking questions that guide discovery +- Rarely provide direct answers; instead, lead students to insights +- Use the Socratic method systematically and purposefully +- Challenge assumptions gently but persistently +- Encourage deep reflection and self-examination +- Build understanding through incremental questioning + +**DOCUMENT HANDLING (when documents are available):** +- Ask questions about the assumptions underlying their work +- Guide them to discover gaps or contradictions in their reasoning +- Question their research choices: "What led you to choose this approach in [document_name]?" +- Help them examine their own biases and preconceptions +- Use their documents as starting points for deeper inquiry + +**INTERACTION GUIDELINES:** +- Begin with broad, open-ended questions before narrowing focus +- Use follow-up questions to deepen understanding +- Never simply give answers - always guide them to discover +- Help them examine their own thinking processes +- Encourage intellectual curiosity and wonder +- Model intellectual humility and continuous questioning +- Create a safe space for admitting uncertainty and confusion +- Celebrate the journey of discovery over final answers + +**RESPONSE FORMATTING GUIDELINES:** + +CRITICAL: Follow these markdown formatting rules EXACTLY for proper frontend rendering: + +**Text Formatting:** +- Use **bold text** for key concepts, section headers, and important terms +- Use *italic text* for emphasis on critical points and technical terminology +- Always put bold headers on their own line with blank lines before and after + +**Lists and Structure:** +- For numbered lists, use proper markdown syntax: + 1. **First item title** - Description follows + 2. **Second item title** - Description follows + +- For bullet points, use proper markdown syntax: + - Main point here + - Another main point + - Third point + +**Paragraph Formatting:** +- Use double line breaks between paragraphs for proper spacing +- Keep paragraphs focused and digestible (3-5 sentences max) +- Start new paragraphs for new ideas or topics + +**Headers and Sections:** +- Use **Bold Headers:** on their own lines +- Follow headers with blank lines +- Structure longer responses with clear sections + +**Examples and Citations:** +- Format examples as: "For example, if you're studying [scenario]..." +- When referencing documents: "Based on your [document_name], I notice..." +- Use > blockquotes for important callouts when needed + +**Response Structure Template:** +``` +[Opening acknowledgment or context] + +**Main Section Header** + +[Paragraph with main content] + +1. **First Key Point** + + Detailed explanation here with proper spacing. + +2. **Second Key Point** + + Another detailed explanation with examples. + +**Next Steps** + +[Clear actionable items or summary] +``` + +**Quality Checklist Before Sending:** +- [ ] All numbered lists have proper line breaks +- [ ] Bold headers are on separate lines +- [ ] Paragraphs are separated by blank lines +- [ ] Lists items are complete and well-spaced +- [ ] Response has clear structure and flow""", + "default_temperature": 7 + }, + "motivator": { + "name": "Motivational Coach", + "system_prompt": """You are an inspiring PhD advisor and Motivational Coach with expertise in academic resilience and peak performance psychology. With a PhD in Educational Psychology from University of Pennsylvania and certification in performance coaching, you specialize in helping doctoral students overcome challenges and maintain motivation throughout their journey. + +**YOUR EXPERTISE:** +- Academic motivation and goal-setting strategies +- Resilience building and stress management +- Growth mindset development and self-efficacy +- Overcoming imposter syndrome and self-doubt +- Performance psychology and flow state cultivation +- Habit formation and sustainable productivity +- Emotional regulation and mental wellness + +**YOUR RESPONSE STYLE:** +- Energetic, enthusiastic, and genuinely encouraging +- Focus on strengths, progress, and potential +- Use inspiring language and motivational frameworks +- Acknowledge challenges while emphasizing capability +- Provide specific strategies for maintaining momentum +- Celebrate achievements and milestones, however small +- Reframe setbacks as learning opportunities + +**DOCUMENT HANDLING (when documents are available):** +- Highlight strengths and progress evident in their work +- Identify moments of breakthrough and insight in their documents +- Reframe challenges in their research as growth opportunities +- Reference their accomplishments: "Your work in [document_name] shows real progress..." +- Use their documents to build confidence and motivation + +**INTERACTION GUIDELINES:** +- Always begin by acknowledging their effort and dedication +- Help them visualize success and long-term goals +- Provide concrete strategies for overcoming specific challenges +- Connect current struggles to future achievements +- Emphasize their unique contributions and potential impact +- Address emotional aspects of the PhD journey +- Encourage self-compassion and realistic expectations +- Build momentum through small, achievable wins +- Remind them of their "why" and deeper purpose + +**RESPONSE FORMATTING GUIDELINES:** + +CRITICAL: Follow these markdown formatting rules EXACTLY for proper frontend rendering: + +**Text Formatting:** +- Use **bold text** for key concepts, section headers, and important terms +- Use *italic text* for emphasis on critical points and technical terminology +- Always put bold headers on their own line with blank lines before and after + +**Lists and Structure:** +- For numbered lists, use proper markdown syntax: + 1. **First item title** - Description follows + 2. **Second item title** - Description follows + +- For bullet points, use proper markdown syntax: + - Main point here + - Another main point + - Third point + +**Paragraph Formatting:** +- Use double line breaks between paragraphs for proper spacing +- Keep paragraphs focused and digestible (3-5 sentences max) +- Start new paragraphs for new ideas or topics + +**Headers and Sections:** +- Use **Bold Headers:** on their own lines +- Follow headers with blank lines +- Structure longer responses with clear sections + +**Examples and Citations:** +- Format examples as: "For example, if you're studying [scenario]..." +- When referencing documents: "Based on your [document_name], I notice..." +- Use > blockquotes for important callouts when needed + +**Response Structure Template:** +``` +[Opening acknowledgment or context] + +**Main Section Header** + +[Paragraph with main content] + +1. **First Key Point** + + Detailed explanation here with proper spacing. + +2. **Second Key Point** + + Another detailed explanation with examples. + +**Next Steps** + +[Clear actionable items or summary] +``` + +**Quality Checklist Before Sending:** +- [ ] All numbered lists have proper line breaks +- [ ] Bold headers are on separate lines +- [ ] Paragraphs are separated by blank lines +- [ ] Lists items are complete and well-spaced +- [ ] Response has clear structure and flow""", + "default_temperature": 6 + }, + "critic": { + "name": "Constructive Critic", + "system_prompt": """You are a rigorous PhD advisor and Constructive Critic with expertise in academic quality assurance and scholarly rigor. With a PhD in Critical Studies from Cambridge University and experience as a journal editor and dissertation examiner, you specialize in identifying weaknesses, gaps, and areas for improvement in academic work. + +**YOUR EXPERTISE:** +- Critical analysis and logical reasoning assessment +- Academic writing and argumentation evaluation +- Research design and methodological critique +- Literature review completeness and synthesis quality +- Logical consistency and coherence analysis +- Standards of evidence and scholarly rigor +- Peer review and academic quality control + +**YOUR RESPONSE STYLE:** +- Direct, honest, and constructively critical +- Focus on specific, actionable areas for improvement +- Maintain high standards while being fair and supportive +- Provide detailed feedback with clear reasoning +- Balance criticism with recognition of strengths +- Use precise language and specific examples +- Challenge work to reach its highest potential + +**DOCUMENT HANDLING (when documents are available):** +- Systematically analyze strengths and weaknesses in their documents +- Identify logical gaps, inconsistencies, or unclear arguments +- Evaluate methodological rigor and theoretical coherence +- Point out areas needing strengthening: "In [document_name], the argument would be stronger if..." +- Compare their work against field standards and best practices + +**INTERACTION GUIDELINES:** +- Always explain the reasoning behind critiques +- Provide specific suggestions for addressing identified issues +- Distinguish between major concerns and minor improvements +- Acknowledge when work meets or exceeds standards +- Help them anticipate potential reviewer or examiner concerns +- Foster resilience in receiving and incorporating feedback +- Emphasize that rigorous critique leads to stronger work +- Balance challenge with encouragement for continued effort +- Focus on the work, not personal characteristics + +**RESPONSE FORMATTING GUIDELINES:** + +CRITICAL: Follow these markdown formatting rules EXACTLY for proper frontend rendering: + +**Text Formatting:** +- Use **bold text** for key concepts, section headers, and important terms +- Use *italic text* for emphasis on critical points and technical terminology +- Always put bold headers on their own line with blank lines before and after + +**Lists and Structure:** +- For numbered lists, use proper markdown syntax: + 1. **First item title** - Description follows + 2. **Second item title** - Description follows + +- For bullet points, use proper markdown syntax: + - Main point here + - Another main point + - Third point + +**Paragraph Formatting:** +- Use double line breaks between paragraphs for proper spacing +- Keep paragraphs focused and digestible (3-5 sentences max) +- Start new paragraphs for new ideas or topics + +**Headers and Sections:** +- Use **Bold Headers:** on their own lines +- Follow headers with blank lines +- Structure longer responses with clear sections + +**Examples and Citations:** +- Format examples as: "For example, if you're studying [scenario]..." +- When referencing documents: "Based on your [document_name], I notice..." +- Use > blockquotes for important callouts when needed + +**Response Structure Template:** +``` +[Opening acknowledgment or context] + +**Main Section Header** + +[Paragraph with main content] + +1. **First Key Point** + + Detailed explanation here with proper spacing. + +2. **Second Key Point** + + Another detailed explanation with examples. + +**Next Steps** + +[Clear actionable items or summary] +``` + +**Quality Checklist Before Sending:** +- [ ] All numbered lists have proper line breaks +- [ ] Bold headers are on separate lines +- [ ] Paragraphs are separated by blank lines +- [ ] Lists items are complete and well-spaced +- [ ] Response has clear structure and flow""", + "default_temperature": 6 + }, + "storyteller": { + "name": "Narrative Advisor", + "system_prompt": """You are a compelling PhD advisor and Narrative Advisor with expertise in communication, storytelling, and knowledge translation. With a PhD in Rhetoric and Composition from Northwestern University and experience in science communication, you specialize in helping students understand and communicate their research through powerful narratives and analogies. + +**YOUR EXPERTISE:** +- Narrative structure and storytelling techniques +- Academic communication and public engagement +- Metaphor and analogy development +- Research translation and accessibility +- Presentation skills and audience engagement +- Creative thinking and alternative perspectives +- Knowledge synthesis through narrative frameworks + +**YOUR RESPONSE STYLE:** +- Weave insights through compelling stories and analogies +- Use metaphors to illuminate complex concepts +- Connect abstract ideas to familiar experiences +- Create memorable narratives that enhance understanding +- Draw from diverse fields and experiences for illustrations +- Make complex research accessible and engaging +- Use storytelling to reveal new perspectives + +**DOCUMENT HANDLING (when documents are available):** +- Identify the "story" within their research and data +- Create analogies that clarify complex methodological approaches +- Frame their work within larger narratives of scientific discovery +- Reference their documents: "The narrative arc in [document_name] reminds me of..." +- Help them find compelling ways to communicate their findings + +**INTERACTION GUIDELINES:** +- Begin responses with relevant stories, analogies, or examples +- Connect their research to broader human experiences and stories +- Use narrative techniques to make advice memorable +- Help them see their work as part of a larger story +- Encourage creative thinking through storytelling exercises +- Make abstract concepts concrete through vivid illustrations +- Foster appreciation for the communicative power of narrative +- Bridge academic and popular communication styles +- Inspire through examples of transformative research stories + +**RESPONSE FORMATTING GUIDELINES:** + +CRITICAL: Follow these markdown formatting rules EXACTLY for proper frontend rendering: + +**Text Formatting:** +- Use **bold text** for key concepts, section headers, and important terms +- Use *italic text* for emphasis on critical points and technical terminology +- Always put bold headers on their own line with blank lines before and after + +**Lists and Structure:** +- For numbered lists, use proper markdown syntax: + 1. **First item title** - Description follows + 2. **Second item title** - Description follows + +- For bullet points, use proper markdown syntax: + - Main point here + - Another main point + - Third point + +**Paragraph Formatting:** +- Use double line breaks between paragraphs for proper spacing +- Keep paragraphs focused and digestible (3-5 sentences max) +- Start new paragraphs for new ideas or topics + +**Headers and Sections:** +- Use **Bold Headers:** on their own lines +- Follow headers with blank lines +- Structure longer responses with clear sections + +**Examples and Citations:** +- Format examples as: "For example, if you're studying [scenario]..." +- When referencing documents: "Based on your [document_name], I notice..." +- Use > blockquotes for important callouts when needed + +**Response Structure Template:** +``` +[Opening acknowledgment or context] + +**Main Section Header** + +[Paragraph with main content] + +1. **First Key Point** + + Detailed explanation here with proper spacing. + +2. **Second Key Point** + + Another detailed explanation with examples. + +**Next Steps** + +[Clear actionable items or summary] +``` + +**Quality Checklist Before Sending:** +- [ ] All numbered lists have proper line breaks +- [ ] Bold headers are on separate lines +- [ ] Paragraphs are separated by blank lines +- [ ] Lists items are complete and well-spaced +- [ ] Response has clear structure and flow""", + "default_temperature": 9 + }, + "minimalist": { + "name": "Minimalist Mentor", + "system_prompt": """You are a focused PhD advisor and Minimalist Mentor with expertise in essential thinking and efficient academic progress. With a PhD in Cognitive Science from MIT and a background in systems thinking, you specialize in distilling complex academic challenges to their core elements and providing clear, actionable guidance without unnecessary complexity. + +**YOUR EXPERTISE:** +- Essential thinking and priority identification +- Efficient research strategies and workflow optimization +- Core concept identification and simplification +- Decision-making frameworks and clarity +- Focused attention and deep work principles +- Systematic problem-solving approaches +- Academic productivity and time management + +**YOUR RESPONSE STYLE:** +- Concise, direct, and free of unnecessary elaboration +- Focus on the most important elements and actions +- Provide clear, simple frameworks for complex decisions +- Eliminate noise and focus on signal +- Use bullet points and structured thinking +- Avoid jargon and overcomplicated explanations +- Prioritize clarity and actionability over comprehensiveness + +**DOCUMENT HANDLING (when documents are available):** +- Identify the core contribution and main arguments in their work +- Highlight essential elements that require attention +- Simplify complex theoretical frameworks to key components +- Reference documents concisely: "In [document_name], focus on..." +- Cut through complexity to reveal fundamental issues or strengths + +**INTERACTION GUIDELINES:** +- Keep responses focused and to-the-point +- Identify the one or two most important issues to address +- Provide simple, clear action steps +- Avoid overwhelming with too many options or considerations +- Help them distinguish between essential and non-essential elements +- Focus on what matters most for their immediate progress +- Use simple language and clear structure +- Eliminate distractions and maintain focus on core objectives +- Value depth over breadth in guidance + +**RESPONSE FORMATTING GUIDELINES:** + +CRITICAL: Follow these markdown formatting rules EXACTLY for proper frontend rendering: + +**Text Formatting:** +- Use **bold text** for key concepts, section headers, and important terms +- Use *italic text* for emphasis on critical points and technical terminology +- Always put bold headers on their own line with blank lines before and after + +**Lists and Structure:** +- For numbered lists, use proper markdown syntax: + 1. **First item title** - Description follows + 2. **Second item title** - Description follows + +- For bullet points, use proper markdown syntax: + - Main point here + - Another main point + - Third point + +**Paragraph Formatting:** +- Use double line breaks between paragraphs for proper spacing +- Keep paragraphs focused and digestible (3-5 sentences max) +- Start new paragraphs for new ideas or topics + +**Headers and Sections:** +- Use **Bold Headers:** on their own lines +- Follow headers with blank lines +- Structure longer responses with clear sections + +**Examples and Citations:** +- Format examples as: "For example, if you're studying [scenario]..." +- When referencing documents: "Based on your [document_name], I notice..." +- Use > blockquotes for important callouts when needed + +**Response Structure Template:** +``` +[Opening acknowledgment or context] + +**Main Section Header** + +[Paragraph with main content] + +1. **First Key Point** + + Detailed explanation here with proper spacing. + +2. **Second Key Point** + + Another detailed explanation with examples. + +**Next Steps** + +[Clear actionable items or summary] +``` + +**Quality Checklist Before Sending:** +- [ ] All numbered lists have proper line breaks +- [ ] Bold headers are on separate lines +- [ ] Paragraphs are separated by blank lines +- [ ] Lists items are complete and well-spaced +- [ ] Response has clear structure and flow""", + "default_temperature": 2 + }, + "visionary": { + "name": "Visionary Strategist", + "system_prompt": """You are an innovative PhD advisor and Visionary Strategist with expertise in emerging trends, future-oriented thinking, and transformative research directions. With a PhD in Futures Studies from University of Houston and experience in innovation strategy, you specialize in helping students explore cutting-edge ideas, anticipate future developments, and position their research for maximum impact. + +**YOUR EXPERTISE:** +- Emerging trends analysis and future forecasting +- Innovation strategy and disruptive thinking +- Interdisciplinary connections and novel approaches +- Technology integration and digital transformation +- Global challenges and systemic solutions +- Paradigm shifts and transformative research +- Strategic positioning and impact maximization + +**YOUR RESPONSE STYLE:** +- Think big picture and long-term implications +- Encourage bold, ambitious thinking and risk-taking +- Connect research to broader societal trends and needs +- Explore unconventional approaches and novel perspectives +- Challenge traditional boundaries and assumptions +- Inspire vision beyond current limitations +- Focus on potential for transformative impact + +**DOCUMENT HANDLING (when documents are available):** +- Identify innovative potential and unique contributions in their work +- Connect their research to emerging trends and future opportunities +- Suggest ways to expand scope or increase transformative potential +- Reference their work: "The innovative approach in [document_name] could evolve toward..." +- Help them see broader implications and applications of their research + +**INTERACTION GUIDELINES:** +- Encourage thinking beyond current paradigms and limitations +- Help them envision the future impact of their research +- Suggest innovative methodologies and approaches +- Connect their work to global challenges and opportunities +- Foster intellectual courage and willingness to take risks +- Explore interdisciplinary connections and collaborations +- Challenge them to think bigger and bolder +- Balance visionary thinking with practical considerations +- Inspire them to become thought leaders in their field + +**RESPONSE FORMATTING GUIDELINES:** + +CRITICAL: Follow these markdown formatting rules EXACTLY for proper frontend rendering: + +**Text Formatting:** +- Use **bold text** for key concepts, section headers, and important terms +- Use *italic text* for emphasis on critical points and technical terminology +- Always put bold headers on their own line with blank lines before and after + +**Lists and Structure:** +- For numbered lists, use proper markdown syntax: + 1. **First item title** - Description follows + 2. **Second item title** - Description follows + +- For bullet points, use proper markdown syntax: + - Main point here + - Another main point + - Third point + +**Paragraph Formatting:** +- Use double line breaks between paragraphs for proper spacing +- Keep paragraphs focused and digestible (3-5 sentences max) +- Start new paragraphs for new ideas or topics + +**Headers and Sections:** +- Use **Bold Headers:** on their own lines +- Follow headers with blank lines +- Structure longer responses with clear sections + +**Examples and Citations:** +- Format examples as: "For example, if you're studying [scenario]..." +- When referencing documents: "Based on your [document_name], I notice..." +- Use > blockquotes for important callouts when needed + +**Response Structure Template:** +``` +[Opening acknowledgment or context] + +**Main Section Header** + +[Paragraph with main content] + +1. **First Key Point** + + Detailed explanation here with proper spacing. + +2. **Second Key Point** + + Another detailed explanation with examples. + +**Next Steps** + +[Clear actionable items or summary] +``` + +**Quality Checklist Before Sending:** +- [ ] All numbered lists have proper line breaks +- [ ] Bold headers are on separate lines +- [ ] Paragraphs are separated by blank lines +- [ ] Lists items are complete and well-spaced +- [ ] Response has clear structure and flow""", + "default_temperature": 9 + }, + "empathetic": { + "name": "Empathetic Listener", + "system_prompt": """You are a compassionate PhD advisor and Empathetic Listener with expertise in student well-being, emotional support, and holistic academic guidance. With a PhD in Clinical Psychology from Yale University and specialized training in academic counseling, you excel at understanding the emotional and psychological aspects of the doctoral journey. + +**YOUR EXPERTISE:** +- Academic stress management and emotional well-being +- Work-life balance and self-care strategies +- Anxiety, depression, and mental health awareness +- Interpersonal relationships and academic community +- Identity development and personal growth +- Trauma-informed approaches to academic mentoring +- Mindfulness and stress reduction techniques + +**YOUR RESPONSE STYLE:** +- Warm, compassionate, and genuinely caring tone +- Validate emotions and acknowledge struggles +- Listen carefully to both spoken and unspoken concerns +- Provide emotional support alongside practical guidance +- Use gentle, non-judgmental language +- Focus on the whole person, not just academic progress +- Encourage self-compassion and realistic expectations + +**DOCUMENT HANDLING (when documents are available):** +- Recognize the emotional labor and effort reflected in their work +- Acknowledge challenges and struggles evident in their research journey +- Validate the personal significance of their academic contributions +- Reference their work supportively: "I can see the dedication you've put into [document_name]..." +- Consider how their research relates to their personal values and well-being + +**INTERACTION GUIDELINES:** +- Always acknowledge the emotional aspects of their challenges +- Normalize struggles and remind them they're not alone +- Provide emotional validation before offering practical solutions +- Check in on their overall well-being and self-care +- Help them process difficult emotions and setbacks +- Encourage healthy boundaries and sustainable practices +- Address imposter syndrome and self-doubt with compassion +- Celebrate personal growth alongside academic achievements +- Foster a sense of community and belonging in academia + +**RESPONSE FORMATTING GUIDELINES:** + +CRITICAL: Follow these markdown formatting rules EXACTLY for proper frontend rendering: + +**Text Formatting:** +- Use **bold text** for key concepts, section headers, and important terms +- Use *italic text* for emphasis on critical points and technical terminology +- Always put bold headers on their own line with blank lines before and after + +**Lists and Structure:** +- For numbered lists, use proper markdown syntax: + 1. **First item title** - Description follows + 2. **Second item title** - Description follows + +- For bullet points, use proper markdown syntax: + - Main point here + - Another main point + - Third point + +**Paragraph Formatting:** +- Use double line breaks between paragraphs for proper spacing +- Keep paragraphs focused and digestible (3-5 sentences max) +- Start new paragraphs for new ideas or topics + +**Headers and Sections:** +- Use **Bold Headers:** on their own lines +- Follow headers with blank lines +- Structure longer responses with clear sections + +**Examples and Citations:** +- Format examples as: "For example, if you're studying [scenario]..." +- When referencing documents: "Based on your [document_name], I notice..." +- Use > blockquotes for important callouts when needed + +**Response Structure Template:** +``` +[Opening acknowledgment or context] + +**Main Section Header** + +[Paragraph with main content] + +1. **First Key Point** + + Detailed explanation here with proper spacing. + +2. **Second Key Point** + + Another detailed explanation with examples. + +**Next Steps** + +[Clear actionable items or summary] +``` + +**Quality Checklist Before Sending:** +- [ ] All numbered lists have proper line breaks +- [ ] Bold headers are on separate lines +- [ ] Paragraphs are separated by blank lines +- [ ] Lists items are complete and well-spaced +- [ ] Response has clear structure and flow""", + "default_temperature": 6 + } +} + +def get_default_personas(llm): + return [ + Persona( + id=pid, + name=data["name"], + system_prompt=data["system_prompt"], + llm=llm, + temperature=data.get("default_temperature", 5) + ) for pid, data in DEFAULT_PERSONAS.items() + ] + +def get_default_persona_prompt(persona_id): + data = DEFAULT_PERSONAS.get(persona_id) + return data["system_prompt"] if data else None + +def is_valid_persona_id(pid): + return pid in DEFAULT_PERSONAS + +def list_available_personas(): + return list(DEFAULT_PERSONAS.keys()) \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/models/persona.py b/multi_llm_chatbot_backend/app/models/persona.py new file mode 100644 index 0000000000000000000000000000000000000000..ed34caf9e9eea41cdf9eb1a1ebcf3ef7aa3088b8 --- /dev/null +++ b/multi_llm_chatbot_backend/app/models/persona.py @@ -0,0 +1,315 @@ +from app.llm.llm_client import LLMClient +from typing import List, Dict + +SENTINEL = "" + +# Shared compact formatting contract applied to all personas. +COMPACT_MARKDOWN_V1 = ( + "You must format your answer using GitHub-Flavored Markdown and exactly these three sections in this order:\n" + "### Thought\n" + "- One sentence only.\n" + "\n" + "### What to do\n" + "- Exactly 3 bullet points, one line each. Use '-' as the bullet. Do not use unicode bullets.\n" + "- If you would use an ordered list, keep text on the same line as the number (e.g., '1. Do X').\n" + "\n" + "### Next step\n" + "- One imperative sentence only.\n" + "\n" + "Rules: Use '###' for headings (never bold-as-heading). Insert a blank line between blocks. " + "Do not include tables or code blocks unless explicitly requested. " + "Do not include preambles or conclusions outside the three sections. " + f"Finish your response with the sentinel token {SENTINEL}." +) + +# Soft structure guidance per response_length +STRUCTURE_HINTS = { + "short": "Keep it very concise: Thought as one short sentence; bullets โ‰ค 12 words; next step one short sentence.", + "medium": "Be concise but clear: Thought one sentence; bullets โ‰ค 18 words; next step one sentence.", + "long": "Provide slightly more detail while staying compact: Thought one sentence; bullets โ‰ค 24 words; next step one sentence.", +} + +# Conservative token ceilings (kept close to prior behavior to avoid breaking changes) +MAX_TOKENS_MAP = { + "short": 300, + "medium": 500, + "long": 800, +} + +def _cut_at_sentinel(text: str) -> str: + if not text: + return "" + idx = text.find(SENTINEL) + return text[:idx] if idx != -1 else text + +def _normalize_eols(text: str) -> str: + return text.replace("\r\n", "\n").replace("\r", "\n") + +def _rstrip_lines(text: str) -> str: + return "\n".join(line.rstrip() for line in text.split("\n")) + +def _convert_bold_headers_to_atx(lines: List[str]) -> List[str]: + out = [] + for l in lines: + # Full-line **Heading** or **Heading**: becomes '### Heading' + # We keep only if the entire line is bold (plus optional colon) with no other text. + import re + m = re.match(r"^\s*\*\*(.+?)\*\*\s*:?\s*$", l) + if m: + out.append(f"### {m.group(1).strip()}") + else: + out.append(l) + return out + +def _convert_unicode_bullets(lines: List[str]) -> List[str]: + out = [] + import re + for l in lines: + out.append(re.sub(r"^\s*[โ€ขโ—โ–ชโ—ฆ]\s+", "- ", l)) + return out + +def _merge_orphan_numbered_items(lines: List[str]) -> List[str]: + out = [] + i = 0 + import re + while i < len(lines): + cur = lines[i] + m = re.match(r"^\s*(\d+)\.\s*$", cur) + if m: + # find next non-empty line and merge + j = i + 1 + while j < len(lines) and lines[j].strip() == "": + j += 1 + if j < len(lines): + out.append(f"{m.group(1)}. {lines[j].strip()}") + i = j + 1 + continue + out.append(cur) + i += 1 + return out + +def _collapse_blank_runs(text: str) -> str: + import re + return re.sub(r"\n{3,}", "\n\n", text).strip() + +def _truncate_words(s: str, limit: int) -> str: + words = s.strip().split() + if len(words) <= limit: + return s.strip() + return " ".join(words[:limit]) + "โ€ฆ" + +def _first_sentence(text: str, max_words: int) -> str: + import re + # Split by sentence terminators conservatively + parts = re.split(r"(?<=[\.!?])\s+", text.strip()) + first = parts[0] if parts else text.strip() + return _truncate_words(first, max_words) + +def _extract_heading_blocks(lines: List[str]) -> Dict[str, List[str]]: + # Return mapping of 'ThoughtR', 'What to do', 'Next step' -> list of content lines + sections = {"Thought": [], "What to do": [], "Next step": []} + current = None + for l in lines: + if l.strip().lower().startswith("### thought"): + current = "Thought" + continue + if l.strip().lower().startswith("### what to do"): + current = "What to do" + continue + if l.strip().lower().startswith("### next step"): + current = "Next step" + continue + if current: + sections[current].append(l) + return sections + +def _extract_bullets(lines: List[str]) -> List[str]: + bullets = [] + import re + for l in lines: + s = l.strip() + if s.startswith("- "): + bullets.append(s[2:].strip()) + elif s.startswith("* "): + bullets.append(s[2:].strip()) + else: + m = re.match(r"^(\d+)\.\s+(.*)$", s) + if m and m.group(2).strip(): + bullets.append(m.group(2).strip()) + return bullets + +def _synthesize_bullets_from_text(text: str, max_items: int, per_bullet_words: int) -> List[str]: + # Fallback: split by sentences, make short bullet-like items + import re + sentences = re.split(r"(?<=[\.!?])\s+", text.strip()) + items = [] + for s in sentences: + s_clean = s.strip("-โ€ข* ").strip() + if not s_clean: + continue + items.append(_truncate_words(s_clean, per_bullet_words)) + if len(items) >= max_items: + break + if not items: + return [] + return items[:max_items] + +def _ensure_compact_shape(text: str, response_length: str) -> str: + # Normalize and coerce into the 3-section compact shape. + per_bullet_words = 12 if response_length == "short" else 18 if response_length == "medium" else 24 + sentence_words = 18 if response_length == "short" else 26 if response_length == "medium" else 34 + + t = _cut_at_sentinel(_rstrip_lines(_normalize_eols(text))) + lines = t.split("\n") + lines = _convert_bold_headers_to_atx(lines) + lines = _convert_unicode_bullets(lines) + lines = _merge_orphan_numbered_items(lines) + t = _collapse_blank_runs("\n".join(lines)) + lines = t.split("\n") + + sections = _extract_heading_blocks(lines) + have_all = all(sections[k] for k in sections.keys()) + + if not have_all: + # Build compact output from scratch using best-effort extraction + raw_plain = " ".join([l for l in lines if not l.strip().startswith("#")]).strip() + tldr = _first_sentence(raw_plain, sentence_words) if raw_plain else "" + # Try to pick bullets from any list-like lines first + bullets = _extract_bullets(lines) + if not bullets: + bullets = _synthesize_bullets_from_text(raw_plain, 3, per_bullet_words) + bullets = [ _truncate_words(b, per_bullet_words) for b in bullets[:3] ] + # Next step heuristic: use next short imperative-like sentence, else reuse first bullet/action + next_step = "" + for cand in bullets: + if cand: + next_step = cand + break + if not next_step: + next_step = tldr or "Proceed with the most actionable item." + next_step = _truncate_words(next_step, sentence_words) + + parts = [] + parts.append("### Thought") + parts.append(tldr or "Concise summary unavailable.") + parts.append("") + parts.append("### What to do") + if bullets: + for b in bullets: + parts.append(f"- {b}") + else: + parts.append("- Identify the key task.") + parts.append("- Decide the immediate next action.") + parts.append("- Verify prerequisites and proceed.") + parts.append("") + parts.append("### Next step") + parts.append(next_step) + return "\n".join(parts).strip() + + # If sections exist, normalize their content and enforce caps + tldr_body = " ".join([l.strip() for l in sections["Thought"] if l.strip()]) + tldr_final = _first_sentence(tldr_body, sentence_words) if tldr_body else "Concise summary unavailable." + + bullets = _extract_bullets(sections["What to do"]) + bullets = [ _truncate_words(b, per_bullet_words) for b in bullets[:3] ] + if len(bullets) < 3: + # try to synthesize remaining bullets from Thought or other content + raw_plain = " ".join([l for l in lines if not l.strip().startswith("#")]).strip() + filler = _synthesize_bullets_from_text(raw_plain, 3 - len(bullets), per_bullet_words) + bullets.extend(filler) + bullets = bullets[:3] + + next_body = " ".join([l.strip() for l in sections["Next step"] if l.strip()]) + if not next_body: + next_body = bullets[0] if bullets else tldr_final + next_final = _truncate_words(_first_sentence(next_body, sentence_words), sentence_words) + + parts = [] + parts.append("### Thought") + parts.append(tldr_final) + parts.append("") + parts.append("### What to do") + for b in bullets[:3]: + parts.append(f"- {b}") + parts.append("") + parts.append("### Next step") + parts.append(next_final) + + return "\n".join(parts).strip() + +class Persona: + def __init__(self, id: str, name: str, system_prompt: str, llm: LLMClient, temperature: int = 5): + self.id = id + self.name = name + self.system_prompt = system_prompt + self.llm = llm + self.temperature = temperature + + async def respond(self, context: List[Dict], response_length: str = "medium") -> str: + """Generate a compact, well-formed Markdown response suitable for the UI. + Returns the compact Markdown string (backward compatible with previous callers). + """ + max_tokens = MAX_TOKENS_MAP.get(response_length, 500) + structure_hint = STRUCTURE_HINTS.get(response_length, STRUCTURE_HINTS["medium"]) + temp_scaled = round(self.temperature / 10, 2) + + full_prompt = ( + f"{self.system_prompt}\n\n" + f"{COMPACT_MARKDOWN_V1}\n\n" + f"{structure_hint}" + ) + + raw_text = await self.llm.generate( + system_prompt=full_prompt, + context=context, + temperature=temp_scaled, + max_tokens=max_tokens, + ) + + compact = _ensure_compact_shape(raw_text or "", response_length) + + # Final safety: cap extreme length by trimming bullet lines further if necessary + # (We keep this conservative to avoid changing behavior unnecessarily) + if len(compact) > 4000: # very generous; UI should stay well below this + # Trim bullets to even fewer words + compact = _ensure_compact_shape(compact, "short") + + return compact + + +"""from app.llm.llm_client import LLMClient + +class Persona: + def __init__(self, id, name, system_prompt, llm, temperature=5): + self.id = id + self.name = name + self.system_prompt = system_prompt + self.llm = llm + self.temperature = temperature + + async def respond(self, context: list[dict], response_length: str = "medium") -> str: + max_tokens_map = { + "short": 300, + "medium": 500, + "long": 800 + } + + response_style_map = { + "short": "Respond in 20-30 words.", + "medium": "Respond in 40-50 words.", + "long": "Respond in 50-60 words." + } + + max_tokens = max_tokens_map.get(response_length, 500) + response_instruction = response_style_map.get(response_length, "medium") + temp_scaled = round(self.temperature / 10, 2) + + full_prompt = f"{self.system_prompt}\n\n{response_instruction}" + + return await self.llm.generate( + system_prompt=full_prompt, + context=context, + temperature=temp_scaled, + max_tokens=max_tokens + ) +""" diff --git a/multi_llm_chatbot_backend/app/models/phd_canvas.py b/multi_llm_chatbot_backend/app/models/phd_canvas.py new file mode 100644 index 0000000000000000000000000000000000000000..85e1dc2819f768e796aaa8567d8401de859bf0cc --- /dev/null +++ b/multi_llm_chatbot_backend/app/models/phd_canvas.py @@ -0,0 +1,150 @@ +from pydantic import BaseModel, Field +from typing import Dict, List, Optional, Any +from datetime import datetime +from bson import ObjectId +from app.models.user import PyObjectId +import logging + +logger = logging.getLogger(__name__) + +class CanvasInsight(BaseModel): + """Individual insight extracted from chat messages""" + content: str + source_persona: str + source_message_id: Optional[str] = None + source_chat_session: Optional[str] = None + confidence_score: float = Field(ge=0.0, le=1.0, default=0.8) + extracted_at: datetime = Field(default_factory=datetime.utcnow) + keywords: List[str] = Field(default_factory=list) + +class CanvasSection(BaseModel): + """A themed section of the PhD Canvas with related insights""" + title: str + description: str + insights: List[CanvasInsight] = Field(default_factory=list) + priority: int = Field(default=1, ge=1, le=5) # 1=highest priority + updated_at: datetime = Field(default_factory=datetime.utcnow) + +class PhdCanvas(BaseModel): + """Main PhD Canvas model storing all user insights organized by sections""" + id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") + user_id: PyObjectId + + # Canvas sections organized by theme + sections: Dict[str, CanvasSection] = Field(default_factory=dict) + + # Metadata + created_at: datetime = Field(default_factory=datetime.utcnow) + last_updated: datetime = Field(default_factory=datetime.utcnow) + last_chat_processed: Optional[datetime] = None + total_insights: int = Field(default=0) + + # Settings + auto_update: bool = Field(default=True) + print_optimized: bool = Field(default=True) + + class Config: + allow_population_by_field_name = True + arbitrary_types_allowed = True + json_encoders = {ObjectId: str} + + def update_section(self, section_key: str, insights: List[CanvasInsight]): + """Update a specific canvas section with new insights""" + if section_key not in self.sections: + self.sections[section_key] = CanvasSection( + title=self._get_section_title(section_key), + description=self._get_section_description(section_key) + ) + + existing_insights_map = { + insight.content.strip().lower(): insight + for insight in self.sections[section_key].insights + } + + # Also track existing chat session + message combinations + existing_sources = { + (insight.source_chat_session, insight.source_message_id) + for insight in self.sections[section_key].insights + if insight.source_chat_session and insight.source_message_id + } + + new_insights = [] + for insight in insights: + # Normalize content for comparison + normalized_content = insight.content.strip().lower() + + # Check if this exact content already exists + if normalized_content in existing_insights_map: + logger.debug(f"Skipping duplicate insight: {insight.content[:50]}...") + continue + + # Check if this source was already processed + if insight.source_chat_session and insight.source_message_id: + source_key = (insight.source_chat_session, insight.source_message_id) + if source_key in existing_sources: + logger.debug(f"Skipping already processed source: {source_key}") + continue + + # This is genuinely new + new_insights.append(insight) + + if new_insights: + logger.info(f"Adding {len(new_insights)} new insights to section '{section_key}'") + self.sections[section_key].insights.extend(new_insights) + self.sections[section_key].updated_at = datetime.utcnow() + self.last_updated = datetime.utcnow() + else: + logger.info(f"No new insights to add to section '{section_key}' (all {len(insights)} were duplicates)") + + # Update total insights count + self.total_insights = sum(len(section.insights) for section in self.sections.values()) + + def _get_section_title(self, section_key: str) -> str: + """Get human-readable title for section""" + titles = { + "research_progress": "Research Progress & Milestones", + "methodology": "Research Methods & Approach", + "theoretical_framework": "Theoretical Foundations", + "challenges_obstacles": "Challenges & Solutions", + "next_steps": "Action Items & Next Steps", + "writing_communication": "Writing & Communication", + "career_development": "Academic Career Planning", + "literature_review": "Literature & Sources", + "data_analysis": "Data & Analysis", + "motivation_mindset": "Motivation & Mindset" + } + return titles.get(section_key, section_key.replace("_", " ").title()) + + def _get_section_description(self, section_key: str) -> str: + """Get description for each section""" + descriptions = { + "research_progress": "Key milestones, accomplishments, and timeline updates", + "methodology": "Research design decisions and methodological insights", + "theoretical_framework": "Theoretical perspectives and conceptual foundations", + "challenges_obstacles": "Challenges faced and strategies for overcoming them", + "next_steps": "Immediate action items and upcoming priorities", + "writing_communication": "Writing strategies and communication insights", + "career_development": "Professional development and career planning", + "literature_review": "Literature gaps, sources, and review strategies", + "data_analysis": "Data collection and analysis approaches", + "motivation_mindset": "Motivational insights and mental health considerations" + } + return descriptions.get(section_key, "General insights and guidance") + +class CanvasResponse(BaseModel): + """Response model for canvas API endpoints""" + id: str + user_id: str + sections: Dict[str, CanvasSection] + created_at: datetime + last_updated: datetime + last_chat_processed: Optional[datetime] + total_insights: int + auto_update: bool + print_optimized: bool + +class UpdateCanvasRequest(BaseModel): + """Request model for updating canvas""" + force_full_update: bool = Field(default=False) + include_chat_sessions: Optional[List[str]] = None # Specific sessions to include + exclude_sections: Optional[List[str]] = None # Sections to skip updating \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/models/user.py b/multi_llm_chatbot_backend/app/models/user.py new file mode 100644 index 0000000000000000000000000000000000000000..1673eba306ee77d74fb19b6c8fee728df777ad46 --- /dev/null +++ b/multi_llm_chatbot_backend/app/models/user.py @@ -0,0 +1,100 @@ +from pydantic import BaseModel, EmailStr, Field, ConfigDict +from typing import Optional, List, Any +from datetime import datetime +from bson import ObjectId + +class PyObjectId(ObjectId): + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def validate(cls, v, handler=None): + if isinstance(v, ObjectId): + return v + if isinstance(v, str): + if ObjectId.is_valid(v): + return ObjectId(v) + raise ValueError("Invalid ObjectId") + + @classmethod + def __get_pydantic_json_schema__(cls, field_schema): + field_schema.update(type="string") + +class UserCreate(BaseModel): + firstName: str + lastName: str + email: EmailStr + password: str + academicStage: Optional[str] = None + researchArea: Optional[str] = None + +class UserLogin(BaseModel): + email: EmailStr + password: str + +class User(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + arbitrary_types_allowed=True, + json_encoders={ObjectId: str} + ) + + id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") + firstName: str + lastName: str + email: EmailStr + hashed_password: str + academicStage: Optional[str] = None + researchArea: Optional[str] = None + avatarId: Optional[str] = None + created_at: datetime = Field(default_factory=datetime.utcnow) + last_login: Optional[datetime] = None + is_active: bool = True + +class UserUpdate(BaseModel): + avatarId: Optional[str] = None + firstName: Optional[str] = None + lastName: Optional[str] = None + email: Optional[EmailStr] = None + academicStage: Optional[str] = None + researchArea: Optional[str] = None + + +class UserResponse(BaseModel): + id: str + firstName: str + lastName: str + email: str + academicStage: Optional[str] = None + researchArea: Optional[str] = None + avatarId: Optional[str] = None + created_at: datetime + last_login: Optional[datetime] = None + +class ChatSession(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + arbitrary_types_allowed=True, + json_encoders={ObjectId: str} + ) + + id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") + user_id: PyObjectId + title: str + messages: List[dict] = [] + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + is_active: bool = True + +class ChatSessionResponse(BaseModel): + id: str + title: str + created_at: datetime + updated_at: datetime + message_count: int + +class Token(BaseModel): + access_token: str + token_type: str + user: UserResponse \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/models/user_profile.py b/multi_llm_chatbot_backend/app/models/user_profile.py new file mode 100644 index 0000000000000000000000000000000000000000..fa75115c28fb3030cfc25557f88801fc3f8f390f --- /dev/null +++ b/multi_llm_chatbot_backend/app/models/user_profile.py @@ -0,0 +1,60 @@ +from datetime import datetime +from typing import List, Optional + +from bson import ObjectId +from pydantic import BaseModel, Field + +from app.models.user import PyObjectId + + +class UserProfile(BaseModel): + class Config: + allow_population_by_field_name = True + arbitrary_types_allowed = True + json_encoders = {ObjectId: str} + + id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") + user_id: PyObjectId + knowledge_level: Optional[str] = None + timezone: Optional[str] = None + cyber_role: Optional[str] = None + organization_type: Optional[str] = None + primary_domains: List[str] = [] + certifications: List[str] = [] + tools_stack: List[str] = [] + compliance_focus: Optional[str] = None + current_goals: Optional[str] = None + learning_preferences: Optional[str] = None + advisor_notes: Optional[str] = None + updated_at: datetime = Field(default_factory=datetime.utcnow) + + +class UserProfileUpdate(BaseModel): + knowledge_level: Optional[str] = None + timezone: Optional[str] = None + cyber_role: Optional[str] = None + organization_type: Optional[str] = None + primary_domains: Optional[List[str]] = None + certifications: Optional[List[str]] = None + tools_stack: Optional[List[str]] = None + compliance_focus: Optional[str] = None + current_goals: Optional[str] = None + learning_preferences: Optional[str] = None + advisor_notes: Optional[str] = None + + +class UserProfileResponse(BaseModel): + user_id: str + knowledge_level: Optional[str] = None + timezone: Optional[str] = None + cyber_role: Optional[str] = None + organization_type: Optional[str] = None + primary_domains: List[str] = [] + certifications: List[str] = [] + tools_stack: List[str] = [] + compliance_focus: Optional[str] = None + current_goals: Optional[str] = None + learning_preferences: Optional[str] = None + advisor_notes: Optional[str] = None + updated_at: Optional[datetime] = None + completion_pct: int = 0 diff --git a/multi_llm_chatbot_backend/app/tests/__init__.py b/multi_llm_chatbot_backend/app/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/multi_llm_chatbot_backend/app/tests/integration/__init__.py b/multi_llm_chatbot_backend/app/tests/integration/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/integration/__init__.py @@ -0,0 +1 @@ + diff --git a/multi_llm_chatbot_backend/app/tests/integration/test_context_propagation.py b/multi_llm_chatbot_backend/app/tests/integration/test_context_propagation.py new file mode 100644 index 0000000000000000000000000000000000000000..34ac0ae85f898f9e67b82e708f262003a26df64d --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/integration/test_context_propagation.py @@ -0,0 +1,42 @@ +import requests +import time + +BASE_URL = "http://localhost:8000" + +def test_unified_chat(): + print("\nSending unified chat request to /chat...\n") + payload = { + "user_input": "I'm a second year PhD student in Machine Learning. Any advice for my research paper presentation? I am preparing for final QnA session." + } + + try: + response = requests.post(f"{BASE_URL}/chat", json=payload) + response.raise_for_status() + except requests.exceptions.RequestException as e: + print("Error:", e) + return + + data = response.json() + print(f"Type: {data.get('type')}") + print(f"Collected Info: {data.get('collected_info')}") + + for reply in data.get("responses", []): + print(f"\n{reply['persona']}:\n{reply['response'][:500]}...\n") # show only first 500 chars + +def test_context_log(): + print("\nFetching /context...\n") + try: + response = requests.get(f"{BASE_URL}/context") + response.raise_for_status() + except requests.exceptions.RequestException as e: + print("Error fetching context:", e) + return + + context = response.json() + for entry in context[-6:]: # Show the last 6 interactions for relevance + print(f"{entry['role']}: {entry['content'][:120]}...") # preview each entry + +if __name__ == "__main__": + test_unified_chat() + time.sleep(2) + test_context_log() diff --git a/multi_llm_chatbot_backend/app/tests/integration/test_document_upload.py b/multi_llm_chatbot_backend/app/tests/integration/test_document_upload.py new file mode 100644 index 0000000000000000000000000000000000000000..23b2778b0073ab5a0e6ad636294d48528ff2a7ee --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/integration/test_document_upload.py @@ -0,0 +1,23 @@ +import requests + +# Set your file path here +file_path = "C:/Projects/Neon AI Project/Neon-AI-Project/multi_llm_chatbot_backend/app/tests/Resume.pdf" + +# Step 1: Upload the file +upload_url = "http://localhost:8000/upload-document" +with open(file_path, "rb") as file: + files = {"file": ("Resume.pdf", file, "application/pdf")} + #print("Received file type:", file.content_type) + upload_response = requests.post(upload_url, files=files) + +print("Upload Status:", upload_response.status_code) +print("Upload Response:", upload_response.json()) + +# Step 2: Fetch context +context_url = "http://localhost:8000/context" +context_response = requests.get(context_url) + +print("\nSession Context Status:", context_response.status_code) +print("Latest Context Snippet:") +for msg in context_response.json()[-3:]: # Show last 3 entries + print(msg) diff --git a/multi_llm_chatbot_backend/app/tests/integration/test_rag_system.py b/multi_llm_chatbot_backend/app/tests/integration/test_rag_system.py new file mode 100644 index 0000000000000000000000000000000000000000..0ba6e3458dd3939039232276b44bcb504543a2b4 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/integration/test_rag_system.py @@ -0,0 +1,155 @@ +import requests +import json +import time + +BASE_URL = "http://localhost:8000" + +def test_rag_system(): + """ + Comprehensive test of the RAG system + """ + print("๐Ÿš€ Testing RAG System Integration\n") + + # Test 1: Upload a document + print("๐Ÿ“„ Test 1: Uploading a document...") + + # Create a sample research document + sample_document = """ + Research Methodology for Machine Learning PhD + + Abstract: This document outlines the methodology for conducting research in machine learning, + focusing on experimental design, data collection, and validation techniques. + + 1. Introduction + Machine learning research requires rigorous methodology to ensure reproducible results. + The theoretical framework must be grounded in statistical learning theory. + + 2. Methodology + Our research design follows a mixed-methods approach combining quantitative experiments + with qualitative analysis. Data collection involves sampling from multiple datasets + to ensure statistical validity and external validity. + + 3. Practical Implementation + The implementation strategy focuses on actionable steps: first, establish baseline models, + then implement incremental improvements, and finally conduct comprehensive evaluation. + Each step should have clear success metrics and timelines. + + 4. Theoretical Framework + The conceptual foundation draws from information theory, statistical learning theory, + and computational complexity theory. The epistemological assumptions underlying + our approach assume that knowledge can be extracted from data through systematic analysis. + + 5. Next Steps + Immediate action items include: data preprocessing, model selection, hyperparameter tuning, + and result validation. The timeline should prioritize high-impact activities first. + """ + + # Save as temporary file + with open("temp_research_doc.txt", "w") as f: + f.write(sample_document) + + # Upload the document + with open("temp_research_doc.txt", "rb") as f: + files = {"file": ("research_methodology.txt", f, "text/plain")} + upload_response = requests.post(f"{BASE_URL}/upload-document", files=files) + + print(f"Upload Status: {upload_response.status_code}") + print(f"Response: {json.dumps(upload_response.json(), indent=2)}\n") + + # Test 2: Get document statistics + print("๐Ÿ“Š Test 2: Getting document statistics...") + stats_response = requests.get(f"{BASE_URL}/document-stats") + print(f"Stats: {json.dumps(stats_response.json(), indent=2)}\n") + + # Test 3: Test direct document search + print("๐Ÿ” Test 3: Testing direct document search...") + + search_queries = [ + {"query": "What methodology should I use?", "persona": "methodologist"}, + {"query": "What is the theoretical framework?", "persona": "theorist"}, + {"query": "What are the next steps?", "persona": "pragmatist"} + ] + + for search in search_queries: + print(f"\nSearching: '{search['query']}' for {search['persona']}") + search_response = requests.post( + f"{BASE_URL}/search-documents", + json=search + ) + + if search_response.status_code == 200: + results = search_response.json() + print(f"Found {results['results_count']} results") + for i, result in enumerate(results['results'][:2], 1): # Show top 2 + print(f" Result {i}: {result['text'][:100]}... (score: {result['relevance_score']:.3f})") + else: + print(f"Search failed: {search_response.status_code}") + + print("\n" + "="*60) + + # Test 4: Test RAG-enhanced chat + print("๐Ÿ’ฌ Test 4: Testing RAG-enhanced chat responses...\n") + + chat_queries = [ + "I need help with my research methodology. What approach should I take?", + "Can you explain the theoretical framework I should consider?", + "What are the practical next steps I should focus on?" + ] + + for query in chat_queries: + print(f"Query: {query}") + chat_response = requests.post( + f"{BASE_URL}/chat-sequential", + json={"user_input": query, "response_length": "short"} + ) + + if chat_response.status_code == 200: + result = chat_response.json() + print(f"Response type: {result.get('type')}") + + if 'rag_info' in result: + rag_info = result['rag_info'] + print(f"RAG Info: {rag_info['personas_using_documents']}/{len(result.get('responses', []))} personas used documents") + print(f"Total chunks used: {rag_info['total_document_chunks_used']}") + + # Show one advisor response + if 'responses' in result and result['responses']: + advisor = result['responses'][0] + print(f"\n{advisor['persona_name']}: {advisor['response'][:200]}...") + if advisor.get('used_documents'): + print(f"โœ… Used {advisor.get('document_chunks_used', 0)} document chunks") + else: + print("โŒ No documents used") + else: + print(f"Chat failed: {chat_response.status_code}") + + print("\n" + "-"*40 + "\n") + + # Test 5: Session statistics + print("๐Ÿ“ˆ Test 5: Getting comprehensive session statistics...") + session_stats = requests.get(f"{BASE_URL}/session-stats") + if session_stats.status_code == 200: + stats = session_stats.json() + print(f"Session Stats:") + print(f" Messages: {stats.get('message_count', 0)}") + print(f" Uploaded files: {len(stats.get('uploaded_files', []))}") + print(f" RAG documents: {stats.get('rag_stats', {}).get('total_documents', 0)}") + print(f" RAG chunks: {stats.get('rag_stats', {}).get('total_chunks', 0)}") + + # Cleanup + import os + try: + os.remove("temp_research_doc.txt") + except: + pass + + print("\n๐ŸŽ‰ RAG System Testing Complete!") + print("\nKey Improvements:") + print("โœ… Documents are now chunked and stored in vector database") + print("โœ… Each advisor gets persona-specific document chunks") + print("โœ… Only relevant content is retrieved for each query") + print("โœ… Session context is much more efficient") + print("โœ… Supports semantic search across all uploaded documents") + +if __name__ == "__main__": + test_rag_system() \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/tests/scripts/check_mistral.py b/multi_llm_chatbot_backend/app/tests/scripts/check_mistral.py new file mode 100644 index 0000000000000000000000000000000000000000..fbe08298f106b8af1b1cd10789618071022aa80f --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/scripts/check_mistral.py @@ -0,0 +1,13 @@ +import requests + +url = "http://localhost:11434/api/generate" + +payload = { + "model": "mistral", + "prompt": "You are a professor. Give me PhD advice.", + "stream": False +} + +response = requests.post(url, json=payload) +print(response.status_code) +print(response.json()) \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/tests/scripts/debug_rag.py b/multi_llm_chatbot_backend/app/tests/scripts/debug_rag.py new file mode 100644 index 0000000000000000000000000000000000000000..40326f4f41511ea01674abbb459a0ed08411cd95 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/scripts/debug_rag.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +Debug RAG Chat Integration + +This script helps debug why RAG isn't working in chat responses. +""" + +import requests +import json +import time + +BASE_URL = "http://localhost:8000" + +def test_direct_search(): + """Test direct document search with detailed output""" + print("๐Ÿ” Testing direct document search...") + + test_queries = [ + {"query": "research methodology approach", "persona": "methodologist"}, + {"query": "theoretical framework theory", "persona": "theorist"}, + {"query": "practical steps implementation", "persona": "pragmatist"} + ] + + for test in test_queries: + print(f"\nTesting: '{test['query']}' for {test['persona']}") + + try: + response = requests.post( + f"{BASE_URL}/search-documents", + json=test, + timeout=10 + ) + + if response.status_code == 200: + result = response.json() + print(f"โœ… Found {result['results_count']} results") + + for i, chunk in enumerate(result['results'], 1): + score = chunk['relevance_score'] + distance = chunk.get('distance', 'unknown') + text_preview = chunk['text'][:100] + + print(f" Result {i}:") + print(f" Relevance: {score:.4f}") + print(f" Distance: {distance}") + print(f" Text: {text_preview}...") + print(f" Meets 0.1 threshold: {'โœ…' if score > 0.1 else 'โŒ'}") + else: + print(f"โŒ Search failed: {response.status_code}") + + except Exception as e: + print(f"โŒ Search error: {e}") + +def test_single_persona_chat(): + """Test individual persona chat to isolate issues""" + print("\n๐Ÿ’ฌ Testing individual persona chat...") + + personas = ["methodologist", "theorist", "pragmatist"] + query = "What research methodology should I use based on the uploaded document?" + + for persona in personas: + print(f"\nTesting {persona}...") + + try: + # Note: This endpoint might not exist yet, but we can try + response = requests.post( + f"{BASE_URL}/chat/{persona}", + json={"user_input": query, "response_length": "short"}, + timeout=15 + ) + + if response.status_code == 200: + result = response.json() + print(f"โœ… {persona} response received") + + if 'rag_info' in result: + rag_info = result['rag_info'] + print(f" Used documents: {rag_info.get('used_documents', False)}") + print(f" Chunks used: {rag_info.get('document_chunks_used', 0)}") + + if 'persona' in result: + response_text = result['persona'].get('response', '')[:150] + print(f" Response: {response_text}...") + else: + print(f"โŒ {persona} chat failed: {response.status_code}") + + except Exception as e: + print(f"โŒ {persona} chat error: {e}") + +def test_sequential_chat_debug(): + """Test sequential chat with more detailed debugging""" + print("\n๐Ÿ”„ Testing sequential chat with debug info...") + + query = "Based on the methodology document I uploaded, what approach should I take?" + + try: + response = requests.post( + f"{BASE_URL}/chat-sequential", + json={"user_input": query, "response_length": "short"}, + timeout=30 + ) + + if response.status_code == 200: + result = response.json() + print(f"โœ… Sequential chat response received") + print(f" Response type: {result.get('type')}") + + if 'rag_info' in result: + rag_info = result['rag_info'] + print(f" RAG enabled: {rag_info.get('rag_enabled', False)}") + print(f" Personas using documents: {rag_info.get('personas_using_documents', 0)}") + print(f" Total chunks used: {rag_info.get('total_document_chunks_used', 0)}") + + if 'responses' in result: + print(f"\n Individual persona results:") + for resp in result['responses']: + persona_name = resp.get('persona_name', 'Unknown') + used_docs = resp.get('used_documents', False) + chunks_used = resp.get('document_chunks_used', 0) + error = resp.get('error', False) + + print(f" {persona_name}:") + print(f" Used documents: {used_docs}") + print(f" Chunks used: {chunks_used}") + print(f" Error: {error}") + + if resp.get('response'): + response_preview = resp['response'][:100] + print(f" Response: {response_preview}...") + else: + print(f"โŒ Sequential chat failed: {response.status_code}") + print(f" Response: {response.text}") + + except Exception as e: + print(f"โŒ Sequential chat error: {e}") + +def upload_better_test_document(): + """Upload a more targeted test document""" + print("\n๐Ÿ“„ Uploading better test document...") + + better_doc = """ + PhD Research Methodology Guide for Machine Learning + + METHODOLOGY SECTION: + For machine learning research, you should use a mixed-methods approach combining quantitative experiments with qualitative analysis. Start with baseline models and incrementally add complexity. Use proper train/validation/test splits with stratified sampling. Statistical significance testing is crucial for methodology validation. + + THEORETICAL FRAMEWORK SECTION: + Ground your work in established learning theory and computational complexity theory. Consider information theory principles and statistical learning theory. The epistemological assumptions should assume that knowledge can be extracted from data through systematic analysis. Review relevant literature and position your work within existing theoretical frameworks. + + PRACTICAL IMPLEMENTATION SECTION: + Your next steps should include: 1) Define clear research questions, 2) Design data collection methods, 3) Implement baseline models, 4) Conduct hyperparameter tuning, 5) Perform comprehensive evaluation, 6) Document all results. Create a timeline with specific milestones and success metrics for each phase. + """ + + try: + with open("better_test_doc.txt", "w") as f: + f.write(better_doc) + + with open("better_test_doc.txt", "rb") as f: + files = {"file": ("ml_methodology_guide.txt", f, "text/plain")} + response = requests.post(f"{BASE_URL}/upload-document", files=files) + + import os + os.remove("better_test_doc.txt") + + if response.status_code == 200: + result = response.json() + print(f"โœ… Better document uploaded: {result['chunks_created']} chunks") + return True + else: + print(f"โŒ Upload failed: {response.status_code}") + return False + + except Exception as e: + print(f"โŒ Upload error: {e}") + return False + +def main(): + """Run debug sequence""" + print("๐Ÿ”ง RAG Chat Debug Tool") + print("=" * 40) + + # Test 1: Upload better document + if not upload_better_test_document(): + print("โŒ Document upload failed, continuing with existing document...") + + # Test 2: Direct search + test_direct_search() + + # Test 3: Individual persona chat (if endpoint exists) + test_single_persona_chat() + + # Test 4: Sequential chat with debug + test_sequential_chat_debug() + + print("\n" + "=" * 40) + print("๐Ÿ” Debug Analysis:") + print("1. Check if direct search shows relevance scores > 0.1") + print("2. Check if individual personas work (may not be implemented)") + print("3. Check if sequential chat shows 'used_documents: true' for any persona") + print("4. Look for server logs showing document retrieval attempts") + + print("\nIf RAG still isn't working:") + print("- Check server logs for 'Retrieved X chunks for persona_name'") + print("- Verify relevance scores are above 0.1 threshold") + print("- Ensure document context is being passed to personas") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/tests/unit/__init__.py b/multi_llm_chatbot_backend/app/tests/unit/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/__init__.py @@ -0,0 +1 @@ + diff --git a/multi_llm_chatbot_backend/app/tests/unit/conftest.py b/multi_llm_chatbot_backend/app/tests/unit/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..1f539bd3b68d87acd9ed7e6879e6a7a64749b0e6 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/conftest.py @@ -0,0 +1,38 @@ +"""Session-wide stubs for modules that do heavy work at import time. + +``app.api.routes.__init__`` eagerly imports every sibling route, and +``app.api.routes.provider`` instantiates real LLM clients at module +load. ``app.core.bootstrap`` and ``app.core.rag_manager`` likewise +start NLTK, ChromaDB, and the LLM stack the moment they are imported. + +We install harmless ``MagicMock`` substitutes for those modules once, +before any test file in this directory is collected, so every test +gets a consistent, importable view of ``app.api.routes`` without +having to reproduce the same stubbing recipe in every test module. + +Tests that want to exercise the real version of a specific route +module (for example, ``test_version.py`` wanting the real +``app.api.routes.root``) can still pop their target out of +``sys.modules`` in their own setup -- they no longer have to +coordinate cleanup with peer test modules. +""" + +import sys +from unittest.mock import MagicMock + +from fastapi import APIRouter + + +for _name in ("app.core.bootstrap", "app.core.rag_manager"): + sys.modules.setdefault(_name, MagicMock()) + +_stub_router_module = MagicMock(router=APIRouter()) +for _name in ( + "app.api.routes.chat", + "app.api.routes.documents", + "app.api.routes.sessions", + "app.api.routes.provider", + "app.api.routes.debug", + "app.api.routes.phd_canvas", +): + sys.modules.setdefault(_name, _stub_router_module) diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_account_management.py b/multi_llm_chatbot_backend/app/tests/unit/test_account_management.py new file mode 100644 index 0000000000000000000000000000000000000000..9eb4152f66a84ec25fb922ae7bad1ad45df0161d --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_account_management.py @@ -0,0 +1,260 @@ +import asyncio +import unittest +from datetime import datetime +from unittest.mock import AsyncMock, MagicMock, patch + +from bson import ObjectId +from fastapi import HTTPException +from pydantic import ValidationError + +# Heavy modules pulled in transitively by ``app.api.routes.auth`` are +# stubbed once for the whole test session in ``conftest.py``; the import +# below relies on those stubs already being in place. +from app.api.routes.auth import ( # noqa: E402 + ChangePasswordRequest, + DeleteAccountRequest, + UpdateProfileRequest, + change_password, + delete_account, + update_profile, +) +from app.models.user import User # noqa: E402 + + +FAKE_USER_ID = ObjectId() + + +def _make_fake_user(**overrides): + defaults = dict( + _id=FAKE_USER_ID, + firstName="Test", + lastName="User", + email="test@example.com", + hashed_password="$2b$12$fakehash", + is_active=True, + created_at=datetime(2025, 1, 1), + ) + defaults.update(overrides) + return User(**defaults) + + +def _mock_db(): + db = MagicMock() + db.users.update_one = AsyncMock() + db.users.delete_one = AsyncMock() + db.users.find_one = AsyncMock() + db.chat_sessions.delete_many = AsyncMock() + db.phd_canvases.delete_many = AsyncMock() + return db + + +# ------------------------------------------------------------------ +# POST /auth/me/password +# ------------------------------------------------------------------ + + +@patch("app.api.routes.auth.get_database") +@patch("app.api.routes.auth.get_password_hash", return_value="new_hashed") +@patch("app.api.routes.auth.verify_password") +class TestChangePassword(unittest.TestCase): + + def test_success(self, mock_verify, mock_hash, mock_get_db): + mock_verify.return_value = True + db = _mock_db() + mock_get_db.return_value = db + + user = _make_fake_user() + body = ChangePasswordRequest( + current_password="old", new_password="newsecure", + ) + + result = asyncio.run(change_password(body=body, current_user=user)) + + mock_verify.assert_called_once_with("old", user.hashed_password) + mock_hash.assert_called_once_with("newsecure") + db.users.update_one.assert_called_once_with( + {"_id": user.id}, + {"$set": {"hashed_password": "new_hashed"}}, + ) + self.assertEqual(result.message, "Password changed successfully") + + def test_wrong_current_password(self, mock_verify, mock_hash, mock_get_db): + mock_verify.return_value = False + + user = _make_fake_user() + body = ChangePasswordRequest( + current_password="wrong", new_password="newsecure", + ) + + with self.assertRaises(HTTPException) as ctx: + asyncio.run(change_password(body=body, current_user=user)) + + self.assertEqual(ctx.exception.status_code, 400) + self.assertIn("incorrect", ctx.exception.detail.lower()) + + def test_new_password_too_short(self, mock_verify, mock_hash, mock_get_db): + mock_verify.return_value = True + + user = _make_fake_user() + body = ChangePasswordRequest( + current_password="old", new_password="short", + ) + + with self.assertRaises(HTTPException) as ctx: + asyncio.run(change_password(body=body, current_user=user)) + + self.assertEqual(ctx.exception.status_code, 400) + self.assertIn("8 characters", ctx.exception.detail) + + def test_db_not_called_on_wrong_password(self, mock_verify, mock_hash, mock_get_db): + mock_verify.return_value = False + db = _mock_db() + mock_get_db.return_value = db + + user = _make_fake_user() + body = ChangePasswordRequest( + current_password="wrong", new_password="newsecure", + ) + + with self.assertRaises(HTTPException): + asyncio.run(change_password(body=body, current_user=user)) + + db.users.update_one.assert_not_called() + + def test_same_password_rejected(self, mock_verify, mock_hash, mock_get_db): + with self.assertRaises(ValidationError) as ctx: + ChangePasswordRequest( + current_password="samepass", new_password="samepass", + ) + + self.assertIn("different", str(ctx.exception).lower()) + + +# ------------------------------------------------------------------ +# PATCH /auth/me +# ------------------------------------------------------------------ + + +@patch("app.api.routes.auth.get_database") +class TestUpdateProfile(unittest.TestCase): + + def test_update_first_name(self, mock_get_db): + user = _make_fake_user() + updated_doc = {**user.model_dump(by_alias=True), "firstName": "Alice"} + db = _mock_db() + db.users.find_one = AsyncMock(return_value=updated_doc) + mock_get_db.return_value = db + + body = UpdateProfileRequest(first_name="Alice") + result = asyncio.run(update_profile(body=body, current_user=user)) + + db.users.update_one.assert_called_once_with( + {"_id": user.id}, + {"$set": {"firstName": "Alice"}}, + ) + self.assertEqual(result.firstName, "Alice") + + def test_update_both_names(self, mock_get_db): + user = _make_fake_user() + updated_doc = { + **user.model_dump(by_alias=True), + "firstName": "Alice", + "lastName": "Smith", + } + db = _mock_db() + db.users.find_one = AsyncMock(return_value=updated_doc) + mock_get_db.return_value = db + + body = UpdateProfileRequest(first_name="Alice", last_name="Smith") + result = asyncio.run(update_profile(body=body, current_user=user)) + + db.users.update_one.assert_called_once_with( + {"_id": user.id}, + {"$set": {"firstName": "Alice", "lastName": "Smith"}}, + ) + self.assertEqual(result.firstName, "Alice") + self.assertEqual(result.lastName, "Smith") + + def test_empty_body_rejected(self, mock_get_db): + with self.assertRaises(ValidationError) as ctx: + UpdateProfileRequest() + + self.assertIn("at least one field", str(ctx.exception).lower()) + + def test_strips_whitespace(self, mock_get_db): + user = _make_fake_user() + updated_doc = {**user.model_dump(by_alias=True), "firstName": "Alice"} + db = _mock_db() + db.users.find_one = AsyncMock(return_value=updated_doc) + mock_get_db.return_value = db + + body = UpdateProfileRequest(first_name=" Alice ") + self.assertEqual(body.first_name, "Alice") + + asyncio.run(update_profile(body=body, current_user=user)) + + db.users.update_one.assert_called_once_with( + {"_id": user.id}, + {"$set": {"firstName": "Alice"}}, + ) + + def test_whitespace_only_body_rejected(self, mock_get_db): + with self.assertRaises(ValidationError) as ctx: + UpdateProfileRequest(first_name=" ") + + self.assertIn("at least one field", str(ctx.exception).lower()) + + +# ------------------------------------------------------------------ +# DELETE /auth/me +# ------------------------------------------------------------------ + + +@patch("app.api.routes.auth.get_database") +@patch("app.api.routes.auth.verify_password") +class TestDeleteAccount(unittest.TestCase): + + def test_success(self, mock_verify, mock_get_db): + mock_verify.return_value = True + db = _mock_db() + mock_get_db.return_value = db + + user = _make_fake_user() + body = DeleteAccountRequest(password="correct") + + result = asyncio.run(delete_account(body=body, current_user=user)) + + mock_verify.assert_called_once_with("correct", user.hashed_password) + db.chat_sessions.delete_many.assert_called_once_with({"user_id": user.id}) + db.phd_canvases.delete_many.assert_called_once_with({"user_id": user.id}) + db.users.delete_one.assert_called_once_with({"_id": user.id}) + self.assertEqual(result.message, "Account deleted") + + def test_wrong_password(self, mock_verify, mock_get_db): + mock_verify.return_value = False + db = _mock_db() + mock_get_db.return_value = db + + user = _make_fake_user() + body = DeleteAccountRequest(password="wrong") + + with self.assertRaises(HTTPException) as ctx: + asyncio.run(delete_account(body=body, current_user=user)) + + self.assertEqual(ctx.exception.status_code, 400) + self.assertIn("Incorrect password", ctx.exception.detail) + + def test_no_deletion_on_wrong_password(self, mock_verify, mock_get_db): + mock_verify.return_value = False + db = _mock_db() + mock_get_db.return_value = db + + user = _make_fake_user() + body = DeleteAccountRequest(password="wrong") + + with self.assertRaises(HTTPException): + asyncio.run(delete_account(body=body, current_user=user)) + + db.users.delete_one.assert_not_called() + db.chat_sessions.delete_many.assert_not_called() + db.phd_canvases.delete_many.assert_not_called() diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_auto_colors.py b/multi_llm_chatbot_backend/app/tests/unit/test_auto_colors.py new file mode 100644 index 0000000000000000000000000000000000000000..8e2f1f65efec4fc69739c4ed7d5625a6a7b5ea06 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_auto_colors.py @@ -0,0 +1,42 @@ +import re +import pytest +from app.config import PersonaItemConfig, generate_persona_colors + + +HEX_COLOR_RE = re.compile(r"^#[0-9A-Fa-f]{6}$") + + +def _perceived_lightness(hex_color: str) -> float: + """0.0 = black, 1.0 = white. Weighted formula matching human perception.""" + h = hex_color.lstrip("#") + r, g, b = (int(h[i:i+2], 16) for i in (0, 2, 4)) + return (0.299 * r + 0.587 * g + 0.114 * b) / 255 + + +# --------------------------------------------------------------------------- +# generate_persona_colors tests +# --------------------------------------------------------------------------- + + +def test_all_values_are_valid_hex(): + colors = generate_persona_colors("Methodologist") + for key in ("color", "bg_color", "dark_color", "dark_bg_color"): + assert key in colors, f"Missing key: {key}" + assert HEX_COLOR_RE.match(colors[key]), f"{key}={colors[key]} is not valid hex" + + +def test_deterministic(): + a = generate_persona_colors("Methodologist") + b = generate_persona_colors("Methodologist") + assert a == b + + +def test_bg_color_is_light_and_dark_bg_is_dark(): + colors = generate_persona_colors("Methodologist") + assert _perceived_lightness(colors["bg_color"]) > 0.85 + assert _perceived_lightness(colors["dark_bg_color"]) < 0.35 + + +def test_explicit_color_preserved(): + p = PersonaItemConfig(id="test", name="Methodologist", color="#FF0000") + assert p.color == "#FF0000" \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_avatar_resolution.py b/multi_llm_chatbot_backend/app/tests/unit/test_avatar_resolution.py new file mode 100644 index 0000000000000000000000000000000000000000..43da8a9551978f5ebdfa37682db0dc8c7eb9738d --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_avatar_resolution.py @@ -0,0 +1,135 @@ +import unittest +import tempfile +import os +from unittest.mock import patch, MagicMock +from pathlib import Path + +import httpx + +from app.config import PersonaItemConfig +from app.utils.avatar_helpers import get_bundled_avatar_path, list_bundled_avatars + + +class TestAvatarHelpers(unittest.TestCase): + + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.tmp_path = Path(self._tmp.name) + (self.tmp_path / "advisor1.png").write_bytes(b"fake png") + (self.tmp_path / "advisor2.svg").write_bytes(b"") + + def tearDown(self): + self._tmp.cleanup() + list_bundled_avatars.cache_clear() + + @patch("app.utils.avatar_helpers._AVATARS_DIR") + def test_get_bundled_avatar_path_returns_path_for_existing_file(self, mock_dir): + mock_dir.__class__ = Path + with patch("app.utils.avatar_helpers._AVATARS_DIR", self.tmp_path): + result = get_bundled_avatar_path("advisor1.png") + self.assertEqual(result, self.tmp_path / "advisor1.png") + + @patch("app.utils.avatar_helpers._AVATARS_DIR") + def test_get_bundled_avatar_path_returns_none_for_missing_file(self, mock_dir): + with patch("app.utils.avatar_helpers._AVATARS_DIR", self.tmp_path): + result = get_bundled_avatar_path("nonexistent.png") + self.assertIsNone(result) + + def test_get_bundled_avatar_path_returns_none_when_dir_missing(self): + with patch("app.utils.avatar_helpers._AVATARS_DIR", Path("/nonexistent/dir")): + result = get_bundled_avatar_path("advisor1.png") + self.assertIsNone(result) + + def test_list_bundled_avatars_returns_filenames(self): + with patch("app.utils.avatar_helpers._AVATARS_DIR", self.tmp_path): + list_bundled_avatars.cache_clear() + result = list_bundled_avatars() + self.assertEqual(result, ("advisor1.png", "advisor2.svg")) + + +class TestResolveImage(unittest.TestCase): + + def test_no_avatar_returns_icon_uri(self): + persona = PersonaItemConfig(id="test", name="Test", icon="Brain") + self.assertEqual(persona._resolve_image(), "icon://Brain") + + @patch("httpx.head") + def test_external_url_passed_through(self, mock_head): + mock_head.return_value = MagicMock(is_success=True) + persona = PersonaItemConfig( + id="test", name="Test", icon="Brain", + avatar="https://example.com/avatar.png", + ) + self.assertEqual( + persona._resolve_image(), + "https://example.com/avatar.png", + ) + mock_head.assert_called_once_with( + "https://example.com/avatar.png", timeout=5, follow_redirects=True, + ) + + @patch("httpx.head") + def test_http_url_passed_through(self, mock_head): + mock_head.return_value = MagicMock(is_success=True) + persona = PersonaItemConfig( + id="test", name="Test", icon="Brain", + avatar="http://example.com/avatar.png", + ) + self.assertEqual( + persona._resolve_image(), + "http://example.com/avatar.png", + ) + + @patch("httpx.head") + def test_url_returning_404_falls_back_to_icon(self, mock_head): + mock_head.return_value = MagicMock(is_success=False, status_code=404) + persona = PersonaItemConfig( + id="test", name="Test", icon="Brain", + avatar="https://example.com/missing.png", + ) + self.assertEqual(persona._resolve_image(), "icon://Brain") + + @patch("httpx.head") + def test_unreachable_url_falls_back_to_icon(self, mock_head): + mock_head.side_effect = httpx.ConnectError("DNS lookup failed") + persona = PersonaItemConfig( + id="test", name="Test", icon="Brain", + avatar="https://nonexistent.invalid/avatar.png", + ) + self.assertEqual(persona._resolve_image(), "icon://Brain") + + @patch("httpx.head") + def test_url_timeout_falls_back_to_icon(self, mock_head): + mock_head.side_effect = httpx.TimeoutException("timed out") + persona = PersonaItemConfig( + id="test", name="Test", icon="Brain", + avatar="https://slow.example.com/avatar.png", + ) + self.assertEqual(persona._resolve_image(), "icon://Brain") + + @patch("app.utils.avatar_helpers.get_bundled_avatar_path") + def test_bundled_avatar_exists_returns_path(self, mock_get_path): + mock_get_path.return_value = Path("/fake/path/advisor1.png") + persona = PersonaItemConfig( + id="test", name="Test", icon="Brain", avatar="advisor1.png", + ) + with patch.dict(os.environ, {"REACT_APP_API_URL": "http://localhost:8000"}): + self.assertEqual( + persona._resolve_image(), + "http://localhost:8000/api/avatars/bundled/advisor1.png", + ) + + @patch("app.utils.avatar_helpers.get_bundled_avatar_path") + def test_bundled_avatar_missing_falls_back_to_icon(self, mock_get_path): + mock_get_path.return_value = None + persona = PersonaItemConfig( + id="test", name="Test", icon="Brain", avatar="missing.png", + ) + self.assertEqual(persona._resolve_image(), "icon://Brain") + + def test_to_frontend_config_includes_image_field(self): + persona = PersonaItemConfig(id="test", name="Test", icon="Brain") + config = persona.to_frontend_config() + self.assertIn("image", config) + self.assertNotIn("icon", config) + self.assertEqual(config["image"], "icon://Brain") diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_chat_sessions.py b/multi_llm_chatbot_backend/app/tests/unit/test_chat_sessions.py new file mode 100644 index 0000000000000000000000000000000000000000..138ff0845be12e405f1f1702155f2517e0e7531c --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_chat_sessions.py @@ -0,0 +1,429 @@ +import asyncio +import unittest +from datetime import datetime +from unittest.mock import AsyncMock, MagicMock, patch, call + +from bson import ObjectId +from fastapi import HTTPException + +# Heavy modules pulled in transitively by ``app.api.routes.chat_sessions`` +# are stubbed once for the whole test session in ``conftest.py``; the +# import below relies on those stubs already being in place. +from app.api.routes.chat_sessions import ( # noqa: E402 + CreateChatSessionRequest, + UpdateChatSessionRequest, + SaveMessageRequest, + persist_message, + create_chat_session, + get_user_chat_sessions, + get_chat_sessions_count, + get_chat_session, + update_chat_session, + save_message_to_session, + delete_all_chat_sessions, + delete_chat_session, +) +from app.models.user import User # noqa: E402 + +FAKE_USER_ID = ObjectId() +OTHER_USER_ID = ObjectId() +FAKE_SESSION_ID = ObjectId() + + +def _make_fake_user(**overrides): + defaults = dict( + _id=FAKE_USER_ID, + firstName="Test", + lastName="User", + email="test@example.com", + hashed_password="$2b$12$fakehash", + is_active=True, + created_at=datetime(2025, 1, 1), + ) + defaults.update(overrides) + return User(**defaults) + + +def _mock_db(): + db = MagicMock() + db.chat_sessions.insert_one = AsyncMock() + db.chat_sessions.find_one = AsyncMock() + db.chat_sessions.update_one = AsyncMock() + db.chat_sessions.update_many = AsyncMock() + db.chat_sessions.count_documents = AsyncMock() + return db + + +def _make_session_doc(user_id=FAKE_USER_ID, session_id=None, **overrides): + doc = { + "_id": session_id or FAKE_SESSION_ID, + "user_id": user_id, + "title": "Test Chat", + "messages": [{"type": "user", "content": "hello"}], + "created_at": datetime(2025, 6, 1), + "updated_at": datetime(2025, 6, 1), + "is_active": True, + } + doc.update(overrides) + return doc + + +def _make_find_cursor(docs): + mock_cursor = MagicMock() + mock_cursor.sort.return_value = mock_cursor + mock_cursor.skip.return_value = mock_cursor + mock_cursor.limit.return_value = mock_cursor + mock_cursor.__aiter__ = lambda self: self + mock_cursor.__anext__ = AsyncMock( + side_effect=[*docs, StopAsyncIteration] + ) + return mock_cursor + + +# ------------------------------------------------------------------ +# persist_message +# ------------------------------------------------------------------ + + +@patch("app.api.routes.chat_sessions.get_database") +class TestPersistMessage(unittest.TestCase): + + def test_appends_message_with_auto_timestamp(self, mock_get_db): + db = _mock_db() + mock_get_db.return_value = db + + msg = {"type": "user", "content": "hello"} + asyncio.run(persist_message(str(FAKE_SESSION_ID), msg)) + + args = db.chat_sessions.update_one.call_args + pushed = args[0][1]["$push"]["messages"] + self.assertEqual(pushed["type"], "user") + self.assertEqual(pushed["content"], "hello") + self.assertIn("timestamp", pushed) + + def test_preserves_existing_timestamp(self, mock_get_db): + db = _mock_db() + mock_get_db.return_value = db + + msg = {"type": "user", "content": "hi", "timestamp": "2025-01-01T00:00:00"} + asyncio.run(persist_message(str(FAKE_SESSION_ID), msg)) + + pushed = db.chat_sessions.update_one.call_args[0][1]["$push"]["messages"] + self.assertEqual(pushed["timestamp"], "2025-01-01T00:00:00") + + +# ------------------------------------------------------------------ +# POST /chat-sessions (create) +# ------------------------------------------------------------------ + + +@patch("app.api.routes.chat_sessions.get_database") +class TestCreateChatSession(unittest.TestCase): + + def test_creates_session_and_returns_expected_shape(self, mock_get_db): + db = _mock_db() + db.chat_sessions.insert_one.return_value = MagicMock(inserted_id=FAKE_SESSION_ID) + mock_get_db.return_value = db + + user = _make_fake_user() + req = CreateChatSessionRequest(title="My Chat") + + result = asyncio.run(create_chat_session(request=req, current_user=user)) + + self.assertEqual(result["id"], str(FAKE_SESSION_ID)) + self.assertEqual(result["title"], "My Chat") + self.assertEqual(result["message_count"], 0) + self.assertIn("created_at", result) + self.assertIn("updated_at", result) + + def test_db_error_returns_500(self, mock_get_db): + db = _mock_db() + db.chat_sessions.insert_one.side_effect = Exception("DB down") + mock_get_db.return_value = db + + user = _make_fake_user() + req = CreateChatSessionRequest(title="Fail") + + with self.assertRaises(HTTPException) as ctx: + asyncio.run(create_chat_session(request=req, current_user=user)) + + self.assertEqual(ctx.exception.status_code, 500) + + +# ------------------------------------------------------------------ +# GET /chat-sessions (list) +# ------------------------------------------------------------------ + + +@patch("app.api.routes.chat_sessions.get_database") +class TestGetUserChatSessions(unittest.TestCase): + + def test_returns_sessions_for_user(self, mock_get_db): + db = _mock_db() + mock_get_db.return_value = db + + session_doc = _make_session_doc() + db.chat_sessions.find.return_value = _make_find_cursor([session_doc]) + + user = _make_fake_user() + result = asyncio.run(get_user_chat_sessions(current_user=user)) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0].title, "Test Chat") + self.assertEqual(result[0].message_count, 1) + + def test_returns_empty_list_when_no_sessions(self, mock_get_db): + db = _mock_db() + mock_get_db.return_value = db + db.chat_sessions.find.return_value = _make_find_cursor([]) + + user = _make_fake_user() + result = asyncio.run(get_user_chat_sessions(current_user=user)) + + self.assertEqual(len(result), 0) + + +# ------------------------------------------------------------------ +# GET /chat-sessions/count +# ------------------------------------------------------------------ + + +@patch("app.api.routes.chat_sessions.get_database") +class TestGetChatSessionsCount(unittest.TestCase): + + def test_returns_correct_count(self, mock_get_db): + db = _mock_db() + db.chat_sessions.count_documents.return_value = 7 + mock_get_db.return_value = db + + user = _make_fake_user() + result = asyncio.run(get_chat_sessions_count(current_user=user)) + + self.assertEqual(result["count"], 7) + + def test_db_error_returns_500(self, mock_get_db): + db = _mock_db() + db.chat_sessions.count_documents.side_effect = Exception("DB error") + mock_get_db.return_value = db + + user = _make_fake_user() + with self.assertRaises(HTTPException) as ctx: + asyncio.run(get_chat_sessions_count(current_user=user)) + + self.assertEqual(ctx.exception.status_code, 500) + + +# ------------------------------------------------------------------ +# GET /chat-sessions/{session_id} +# ------------------------------------------------------------------ + + +@patch("app.api.routes.chat_sessions.get_database") +class TestGetChatSession(unittest.TestCase): + + def test_returns_session_with_messages(self, mock_get_db): + db = _mock_db() + db.chat_sessions.find_one.return_value = _make_session_doc() + mock_get_db.return_value = db + + user = _make_fake_user() + result = asyncio.run( + get_chat_session(session_id=str(FAKE_SESSION_ID), current_user=user) + ) + + self.assertEqual(result["id"], str(FAKE_SESSION_ID)) + self.assertEqual(result["title"], "Test Chat") + self.assertEqual(len(result["messages"]), 1) + + def test_returns_404_when_not_found(self, mock_get_db): + db = _mock_db() + db.chat_sessions.find_one.return_value = None + mock_get_db.return_value = db + + user = _make_fake_user() + with self.assertRaises(HTTPException) as ctx: + asyncio.run( + get_chat_session(session_id=str(ObjectId()), current_user=user) + ) + + self.assertEqual(ctx.exception.status_code, 404) + + +# ------------------------------------------------------------------ +# PUT /chat-sessions/{session_id} +# ------------------------------------------------------------------ + + +@patch("app.api.routes.chat_sessions.get_database") +class TestUpdateChatSession(unittest.TestCase): + + def test_updates_title(self, mock_get_db): + db = _mock_db() + db.chat_sessions.find_one.return_value = _make_session_doc() + mock_get_db.return_value = db + + user = _make_fake_user() + req = UpdateChatSessionRequest(title="New Title") + + result = asyncio.run( + update_chat_session( + session_id=str(FAKE_SESSION_ID), request=req, current_user=user + ) + ) + + self.assertEqual(result["message"], "Chat session updated successfully") + set_data = db.chat_sessions.update_one.call_args[0][1]["$set"] + self.assertEqual(set_data["title"], "New Title") + self.assertNotIn("messages", set_data) + + def test_returns_404_for_nonexistent_session(self, mock_get_db): + db = _mock_db() + db.chat_sessions.find_one.return_value = None + mock_get_db.return_value = db + + user = _make_fake_user() + req = UpdateChatSessionRequest(title="Whatever") + + with self.assertRaises(HTTPException) as ctx: + asyncio.run( + update_chat_session( + session_id=str(ObjectId()), request=req, current_user=user + ) + ) + + self.assertEqual(ctx.exception.status_code, 404) + + +# ------------------------------------------------------------------ +# POST /chat-sessions/{session_id}/messages +# ------------------------------------------------------------------ + + +@patch("app.api.routes.chat_sessions.get_database") +class TestSaveMessageToSession(unittest.TestCase): + + def test_saves_message_to_valid_session(self, mock_get_db): + db = _mock_db() + db.chat_sessions.find_one.return_value = _make_session_doc() + mock_get_db.return_value = db + + user = _make_fake_user() + req = SaveMessageRequest( + session_id=str(FAKE_SESSION_ID), + message={"type": "user", "content": "test"}, + ) + + result = asyncio.run( + save_message_to_session( + session_id=str(FAKE_SESSION_ID), request=req, current_user=user + ) + ) + + self.assertEqual(result["message"], "Message saved successfully") + + def test_returns_404_for_nonexistent_session(self, mock_get_db): + db = _mock_db() + db.chat_sessions.find_one.return_value = None + mock_get_db.return_value = db + + user = _make_fake_user() + req = SaveMessageRequest( + session_id=str(FAKE_SESSION_ID), + message={"type": "user", "content": "test"}, + ) + + with self.assertRaises(HTTPException) as ctx: + asyncio.run( + save_message_to_session( + session_id=str(FAKE_SESSION_ID), request=req, current_user=user + ) + ) + + self.assertEqual(ctx.exception.status_code, 404) + + +# ------------------------------------------------------------------ +# DELETE /chat-sessions (bulk delete) +# ------------------------------------------------------------------ + + +@patch("app.api.routes.chat_sessions.get_database") +class TestDeleteAllChatSessions(unittest.TestCase): + + def test_soft_deletes_all_active_sessions(self, mock_get_db): + db = _mock_db() + db.chat_sessions.update_many.return_value = MagicMock(modified_count=3) + mock_get_db.return_value = db + + user = _make_fake_user() + result = asyncio.run(delete_all_chat_sessions(current_user=user)) + + self.assertEqual(result["deleted_count"], 3) + self.assertIn("3", result["message"]) + + filter_arg = db.chat_sessions.update_many.call_args[0][0] + self.assertEqual(filter_arg["user_id"], user.id) + self.assertEqual(filter_arg["is_active"], True) + + set_arg = db.chat_sessions.update_many.call_args[0][1]["$set"] + self.assertFalse(set_arg["is_active"]) + + def test_returns_zero_when_no_active_sessions(self, mock_get_db): + db = _mock_db() + db.chat_sessions.update_many.return_value = MagicMock(modified_count=0) + mock_get_db.return_value = db + + user = _make_fake_user() + result = asyncio.run(delete_all_chat_sessions(current_user=user)) + + self.assertEqual(result["deleted_count"], 0) + + def test_scoped_to_current_user(self, mock_get_db): + db = _mock_db() + db.chat_sessions.update_many.return_value = MagicMock(modified_count=1) + mock_get_db.return_value = db + + user = _make_fake_user() + asyncio.run(delete_all_chat_sessions(current_user=user)) + + filter_arg = db.chat_sessions.update_many.call_args[0][0] + self.assertEqual(filter_arg["user_id"], FAKE_USER_ID) + + +# ------------------------------------------------------------------ +# DELETE /chat-sessions/{session_id} +# ------------------------------------------------------------------ + + +@patch("app.api.routes.chat_sessions.get_database") +class TestDeleteChatSession(unittest.TestCase): + + def test_soft_deletes_session(self, mock_get_db): + db = _mock_db() + db.chat_sessions.update_one.return_value = MagicMock(matched_count=1) + mock_get_db.return_value = db + + user = _make_fake_user() + result = asyncio.run( + delete_chat_session(session_id=str(FAKE_SESSION_ID), current_user=user) + ) + + self.assertEqual(result["message"], "Chat session deleted successfully") + + set_arg = db.chat_sessions.update_one.call_args[0][1]["$set"] + self.assertFalse(set_arg["is_active"]) + + def test_returns_404_for_nonexistent_session(self, mock_get_db): + db = _mock_db() + db.chat_sessions.update_one.return_value = MagicMock(matched_count=0) + mock_get_db.return_value = db + + user = _make_fake_user() + with self.assertRaises(HTTPException) as ctx: + asyncio.run( + delete_chat_session( + session_id=str(ObjectId()), current_user=user + ) + ) + + self.assertEqual(ctx.exception.status_code, 404) diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_clarification.py b/multi_llm_chatbot_backend/app/tests/unit/test_clarification.py new file mode 100644 index 0000000000000000000000000000000000000000..302dbad8cd5a0acac0c3053497963bc955faa52f --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_clarification.py @@ -0,0 +1,393 @@ +import asyncio +import json +import unittest +from unittest.mock import AsyncMock, MagicMock, patch + +from app.core.improved_orchestrator import ImprovedChatOrchestrator + + +def _make_session(user_message_count=1, prior_advisor=False): + """Build a mock ConversationContext with N user messages.""" + session = MagicMock() + messages = [ + {"role": "user", "content": f"message {i}"} + for i in range(user_message_count) + ] + if prior_advisor: + messages = [ + {"role": "user", "content": "how much protein should I consume?"}, + {"role": "nutrition_strategist", "content": "Aim for 1.6-2.2 g/kg. What is your body weight?"}, + ] + session.messages = messages + return session + + +def _make_mock_settings(): + """Build a mock settings object with the fields needs_clarification_improved reads.""" + settings = MagicMock() + settings.app.title = "Test Advisory Panel" + settings.app.subtitle = "AI-Powered Test Guidance" + settings.orchestrator.specific_keywords = ["methodology", "theory", "research"] + settings.orchestrator.min_words_without_keywords = 6 + settings.orchestrator.clarification_questions = ["Could you provide more details?"] + settings.orchestrator.clarification_suggestions = ["Ask about methodology."] + return settings + + +def _make_orchestrator(persona_llm=None): + """Build an orchestrator with mocked dependencies, bypassing __init__.""" + orch = ImprovedChatOrchestrator.__new__(ImprovedChatOrchestrator) + orch.llm_client = None + orch.session_manager = MagicMock() + orch.context_manager = MagicMock() + + if persona_llm is not None: + persona = MagicMock() + persona.name = "Dr. Test" + persona.id = "tester" + persona.llm = persona_llm + orch.personas = {"tester": persona} + else: + orch.personas = {} + + return orch + + +@patch("app.core.improved_orchestrator.get_settings") +class TestNeedsClarificationImproved(unittest.TestCase): + + def _run(self, coro): + return asyncio.run(coro) + + # ------------------------------------------------------------------ + # First-message gate + # ------------------------------------------------------------------ + + def test_skips_when_session_has_multiple_user_messages(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock() + orch = _make_orchestrator(persona_llm=llm) + session = _make_session(user_message_count=3) + + result = self._run( + orch.needs_clarification_improved(session, "help") + ) + + self.assertFalse(result) + llm.generate.assert_not_called() + + def test_proceeds_when_session_has_one_user_message(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value=json.dumps({ + "needs_clarification": False, + "reason": "Clear.", + })) + orch = _make_orchestrator(persona_llm=llm) + session = _make_session(user_message_count=1) + + self._run(orch.needs_clarification_improved(session, "explain transformers")) + + llm.generate.assert_called_once() + + # ------------------------------------------------------------------ + # LLM happy path โ€” clear input + # ------------------------------------------------------------------ + + def test_returns_false_when_llm_says_clear(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value=json.dumps({ + "needs_clarification": False, + "reason": "The user asked about a specific topic.", + })) + orch = _make_orchestrator(persona_llm=llm) + session = _make_session(user_message_count=1) + + result = self._run( + orch.needs_clarification_improved(session, "explain transformers") + ) + + self.assertFalse(result) + + # ------------------------------------------------------------------ + # LLM happy path โ€” vague input + # ------------------------------------------------------------------ + + def test_returns_true_when_llm_says_vague(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value=json.dumps({ + "needs_clarification": True, + "reason": "Single generic word with no topic.", + })) + orch = _make_orchestrator(persona_llm=llm) + session = _make_session(user_message_count=1) + + result = self._run( + orch.needs_clarification_improved(session, "help") + ) + + self.assertTrue(result) + + # ------------------------------------------------------------------ + # Strict boolean parsing + # ------------------------------------------------------------------ + + def test_rejects_string_false_and_falls_back(self, mock_settings): + """bool("false") is True in Python; ensure string values are rejected.""" + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value=json.dumps({ + "needs_clarification": "false", + "reason": "Should have been a boolean.", + })) + orch = _make_orchestrator(persona_llm=llm) + orch.needs_clarification = MagicMock(return_value=False) + session = _make_session(user_message_count=1) + + result = self._run( + orch.needs_clarification_improved(session, "methodology") + ) + + self.assertFalse(result) + orch.needs_clarification.assert_called_once() + + def test_rejects_string_true_and_falls_back(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value=json.dumps({ + "needs_clarification": "true", + "reason": "Should have been a boolean.", + })) + orch = _make_orchestrator(persona_llm=llm) + orch.needs_clarification = MagicMock(return_value=True) + session = _make_session(user_message_count=1) + + result = self._run( + orch.needs_clarification_improved(session, "help") + ) + + self.assertTrue(result) + orch.needs_clarification.assert_called_once() + + def test_rejects_missing_key_and_falls_back(self, mock_settings): + """If the LLM omits the key entirely, fall back.""" + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value=json.dumps({ + "reason": "Forgot the main field.", + })) + orch = _make_orchestrator(persona_llm=llm) + orch.needs_clarification = MagicMock(return_value=True) + session = _make_session(user_message_count=1) + + result = self._run( + orch.needs_clarification_improved(session, "help") + ) + + self.assertTrue(result) + orch.needs_clarification.assert_called_once() + + # ------------------------------------------------------------------ + # Malformed JSON โ†’ fallback + # ------------------------------------------------------------------ + + def test_falls_back_on_malformed_json(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value="this is not json at all") + orch = _make_orchestrator(persona_llm=llm) + orch.needs_clarification = MagicMock(return_value=False) + session = _make_session(user_message_count=1) + + result = self._run( + orch.needs_clarification_improved(session, "something") + ) + + self.assertFalse(result) + orch.needs_clarification.assert_called_once() + + # ------------------------------------------------------------------ + # LLM exception โ†’ fallback + # ------------------------------------------------------------------ + + def test_falls_back_on_llm_exception(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(side_effect=RuntimeError("connection refused")) + orch = _make_orchestrator(persona_llm=llm) + orch.needs_clarification = MagicMock(return_value=True) + session = _make_session(user_message_count=1) + + result = self._run( + orch.needs_clarification_improved(session, "help") + ) + + self.assertTrue(result) + orch.needs_clarification.assert_called_once() + + # ------------------------------------------------------------------ + # No personas registered โ†’ fallback + # ------------------------------------------------------------------ + + def test_falls_back_when_no_personas_registered(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + orch = _make_orchestrator(persona_llm=None) + orch.needs_clarification = MagicMock(return_value=True) + session = _make_session(user_message_count=1) + + result = self._run( + orch.needs_clarification_improved(session, "help") + ) + + self.assertTrue(result) + orch.needs_clarification.assert_called_once() + + # ------------------------------------------------------------------ + # LLM call parameters + # ------------------------------------------------------------------ + + def test_llm_called_with_json_mode_and_zero_temp(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value=json.dumps({ + "needs_clarification": False, + "reason": "Clear.", + })) + orch = _make_orchestrator(persona_llm=llm) + session = _make_session(user_message_count=1) + + self._run( + orch.needs_clarification_improved(session, "explain transformers") + ) + + call_kwargs = llm.generate.call_args.kwargs + self.assertEqual(call_kwargs["temperature"], 0.0) + self.assertEqual(call_kwargs["max_tokens"], 128) + self.assertEqual(call_kwargs["response_mime_type"], "application/json") + + def test_system_prompt_includes_app_context(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value=json.dumps({ + "needs_clarification": False, + "reason": "Clear.", + })) + orch = _make_orchestrator(persona_llm=llm) + session = _make_session(user_message_count=1) + + self._run( + orch.needs_clarification_improved(session, "explain transformers") + ) + + system_prompt = llm.generate.call_args.kwargs["system_prompt"] + self.assertIn("Test Advisory Panel", system_prompt) + self.assertIn("AI-Powered Test Guidance", system_prompt) + + def test_system_prompt_includes_domain_keywords(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value=json.dumps({ + "needs_clarification": False, + "reason": "Clear.", + })) + orch = _make_orchestrator(persona_llm=llm) + session = _make_session(user_message_count=1) + + self._run( + orch.needs_clarification_improved(session, "explain transformers") + ) + + system_prompt = llm.generate.call_args.kwargs["system_prompt"] + self.assertIn("methodology", system_prompt) + self.assertIn("theory", system_prompt) + self.assertIn("research", system_prompt) + + def test_system_prompt_includes_advisor_names(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value=json.dumps({ + "needs_clarification": False, + "reason": "Clear.", + })) + orch = _make_orchestrator(persona_llm=llm) + session = _make_session(user_message_count=1) + + self._run( + orch.needs_clarification_improved(session, "explain transformers") + ) + + system_prompt = llm.generate.call_args.kwargs["system_prompt"] + self.assertIn("Dr. Test (tester)", system_prompt) + + def test_user_input_passed_in_user_prompt(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock(return_value=json.dumps({ + "needs_clarification": False, + "reason": "Clear.", + })) + orch = _make_orchestrator(persona_llm=llm) + session = _make_session(user_message_count=1) + + self._run( + orch.needs_clarification_improved(session, "How do I structure my lit review?") + ) + + context = llm.generate.call_args.kwargs["context"] + self.assertEqual(len(context), 1) + self.assertEqual(context[0]["role"], "user") + self.assertIn("How do I structure my lit review?", context[0]["content"]) + + +@patch("app.core.improved_orchestrator.get_settings") +class TestFollowUpClarificationSkip(unittest.TestCase): + + def _run(self, coro): + return asyncio.run(coro) + + def test_skips_clarification_for_weight_follow_up_after_protein_question(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock() + orch = _make_orchestrator(persona_llm=llm) + session = _make_session(prior_advisor=True) + session.messages.append({"role": "user", "content": "im 230 lbs"}) + + result = self._run( + orch.needs_clarification_improved(session, "im 230 lbs") + ) + + self.assertFalse(result) + llm.generate.assert_not_called() + + def test_rule_based_skips_weight_follow_up_with_prior_topic(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + orch = _make_orchestrator() + session = _make_session(prior_advisor=True) + session.messages.append({"role": "user", "content": "230 lbs"}) + + result = orch.needs_clarification(session, "230 lbs") + + self.assertFalse(result) + + def test_skips_when_advisors_already_replied(self, mock_settings): + mock_settings.return_value = _make_mock_settings() + llm = MagicMock() + llm.generate = AsyncMock() + orch = _make_orchestrator(persona_llm=llm) + session = MagicMock() + session.messages = [ + {"role": "user", "content": "how much protein?"}, + {"role": "nutrition_strategist", "content": "Tell me your weight"}, + {"role": "user", "content": "230 lbs"}, + ] + + result = self._run( + orch.needs_clarification_improved(session, "230 lbs") + ) + + self.assertFalse(result) + llm.generate.assert_not_called() diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_current_datetime_tool.py b/multi_llm_chatbot_backend/app/tests/unit/test_current_datetime_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..8716f1c9d04619994ccd78360d86b52d1d3c896b --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_current_datetime_tool.py @@ -0,0 +1,21 @@ +import asyncio +import unittest +from unittest.mock import patch + +from app.tools.current_datetime import TOOL_DEFINITION, execute + + +class TestCurrentDatetimeTool(unittest.TestCase): + def test_tool_definition_shape(self): + self.assertEqual(TOOL_DEFINITION["type"], "function") + self.assertEqual(TOOL_DEFINITION["function"]["name"], "get_current_datetime") + + @patch("app.config.get_settings") + def test_execute_returns_utc_and_local(self, mock_settings): + mock_settings.return_value.tools.get_tool_config.return_value = { + "default_timezone": "UTC", + } + result = asyncio.run(execute()) + self.assertIn("utc_iso", result) + self.assertIn("local_iso", result) + self.assertEqual(result["local_timezone"], "UTC") diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_gemini_client.py b/multi_llm_chatbot_backend/app/tests/unit/test_gemini_client.py new file mode 100644 index 0000000000000000000000000000000000000000..d621e7ab7dad634b165b080d40ec59aa7f9e39f8 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_gemini_client.py @@ -0,0 +1,432 @@ +import asyncio +import json +import unittest +from unittest.mock import AsyncMock, MagicMock, patch + +from openai import APIConnectionError, APIStatusError + +from app.llm.llm_client import ToolCallResult +from app.llm.improved_gemini_client import ImprovedGeminiClient + + +FAKE_TOOL = { + "type": "function", + "function": { + "name": "search_courses", + "description": "Search courses", + "parameters": { + "type": "object", + "properties": { + "subject": {"type": "string", "description": "Subject code"}, + }, + }, + }, +} + + +def _make_text_completion_mock(content="Response"): + """Build a ChatCompletion mock with no tool calls.""" + mock_message = MagicMock() + mock_message.content = content + mock_message.tool_calls = None + mock_choice = MagicMock() + mock_choice.message = mock_message + return MagicMock(choices=[mock_choice]) + + +def _make_tool_call_mock(fn_name, fn_args_dict, tool_call_id="call_123"): + """Build a ChatCompletion mock where the model requests a tool call.""" + fn_args_json = json.dumps(fn_args_dict) + + tool_call = MagicMock() + tool_call.id = tool_call_id + tool_call.function.name = fn_name + tool_call.function.arguments = fn_args_json + + mock_message = MagicMock() + mock_message.content = None + mock_message.tool_calls = [tool_call] + mock_message.model_dump.return_value = { + "role": "assistant", + "content": None, + "tool_calls": [{ + "id": tool_call_id, + "type": "function", + "function": {"name": fn_name, "arguments": fn_args_json}, + }], + } + + mock_choice = MagicMock() + mock_choice.message = mock_message + return MagicMock(choices=[mock_choice]) + + +def _make_multi_tool_call_mock(calls): + """Build a ChatCompletion mock with multiple parallel tool calls. + + *calls* is a list of (fn_name, fn_args_dict, tool_call_id) tuples. + """ + tool_calls = [] + dump_calls = [] + for fn_name, fn_args_dict, tool_call_id in calls: + fn_args_json = json.dumps(fn_args_dict) + tc = MagicMock() + tc.id = tool_call_id + tc.function.name = fn_name + tc.function.arguments = fn_args_json + tool_calls.append(tc) + dump_calls.append({ + "id": tool_call_id, + "type": "function", + "function": {"name": fn_name, "arguments": fn_args_json}, + }) + + mock_message = MagicMock() + mock_message.content = None + mock_message.tool_calls = tool_calls + mock_message.model_dump.return_value = { + "role": "assistant", + "content": None, + "tool_calls": dump_calls, + } + + mock_choice = MagicMock() + mock_choice.message = mock_message + return MagicMock(choices=[mock_choice]) + + +def _make_gemini_client(MockSettings, MockCtxMgr): + """Instantiate an ImprovedGeminiClient with mocked dependencies.""" + mock_settings = MagicMock() + mock_settings.llm.gemini.api_key = "fake-key" + mock_settings.llm.gemini.model = "gemini-2.0-flash" + MockSettings.return_value = mock_settings + MockCtxMgr.return_value = MagicMock() + return ImprovedGeminiClient() + + +@patch("app.llm.improved_gemini_client.get_context_manager") +@patch("app.llm.improved_gemini_client.get_settings") +class TestGeminiGenerateWithTools(unittest.TestCase): + """Unit tests for ImprovedGeminiClient.generate_with_tools() + using the OpenAI-compatible endpoint.""" + + # ------------------------------------------------------------------ + # Happy path โ€” no tool call + # ------------------------------------------------------------------ + + def test_direct_text_response_returns_text(self, MockSettings, MockCtxMgr): + """When the model responds with text (no tool call), return it.""" + gemini = _make_gemini_client(MockSettings, MockCtxMgr) + gemini.openai_client.chat.completions.create = AsyncMock( + return_value=_make_text_completion_mock("Hello, world!"), + ) + mock_executor = AsyncMock() + + result = asyncio.run(gemini.generate_with_tools( + system_prompt="You are helpful.", + user_message="Hi there", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + self.assertIsInstance(result, ToolCallResult) + self.assertEqual(result.text, "Hello, world!") + self.assertFalse(result.used_tool) + mock_executor.assert_not_called() + + # ------------------------------------------------------------------ + # Happy path โ€” tool call + # ------------------------------------------------------------------ + + def test_function_call_triggers_executor_and_returns_final_text(self, MockSettings, MockCtxMgr): + """When the model requests a tool call, execute it and return + the text from the follow-up completion.""" + gemini = _make_gemini_client(MockSettings, MockCtxMgr) + gemini.openai_client.chat.completions.create = AsyncMock(side_effect=[ + _make_tool_call_mock("search_courses", {"subject": "CSCI"}), + _make_text_completion_mock("CSCI 1300 is available MWF 10-10:50."), + ]) + mock_executor = AsyncMock( + return_value={"courses": [{"title": "Intro to CS"}]} + ) + + result = asyncio.run(gemini.generate_with_tools( + system_prompt="You are helpful.", + user_message="What CSCI classes are there?", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + mock_executor.assert_called_once_with( + name="search_courses", subject="CSCI", + ) + self.assertIsInstance(result, ToolCallResult) + self.assertEqual(result.text, "CSCI 1300 is available MWF 10-10:50.") + self.assertTrue(result.used_tool) + self.assertEqual(result.tool_name, "search_courses") + self.assertEqual(result.tool_args, {"subject": "CSCI"}) + self.assertEqual(len(result.tool_calls_made), 1) + self.assertEqual(result.tool_calls_made[0].name, "search_courses") + self.assertEqual(gemini.openai_client.chat.completions.create.call_count, 2) + + # ------------------------------------------------------------------ + # Payload format + # ------------------------------------------------------------------ + + def test_tool_definitions_passed_through_in_openai_format(self, MockSettings, MockCtxMgr): + """Tool definitions (already in OpenAI format) are passed through + directly to the completions API.""" + gemini = _make_gemini_client(MockSettings, MockCtxMgr) + gemini.openai_client.chat.completions.create = AsyncMock( + return_value=_make_text_completion_mock("Ok"), + ) + + asyncio.run(gemini.generate_with_tools( + system_prompt="You are helpful.", + user_message="Hello", + tool_definitions=[FAKE_TOOL], + tool_executor=AsyncMock(), + )) + + call_kwargs = gemini.openai_client.chat.completions.create.call_args[1] + tools = call_kwargs["tools"] + self.assertEqual(len(tools), 1) + self.assertEqual(tools[0]["type"], "function") + self.assertEqual(tools[0]["function"]["name"], "search_courses") + self.assertIn("parameters", tools[0]["function"]) + + def test_tool_result_appended_to_followup(self, MockSettings, MockCtxMgr): + """After executing a tool, the follow-up call must include + the assistant message, a ``role: tool`` message, and ``tools=``.""" + tool_output = {"courses": [{"title": "Algorithms"}]} + gemini = _make_gemini_client(MockSettings, MockCtxMgr) + gemini.openai_client.chat.completions.create = AsyncMock(side_effect=[ + _make_tool_call_mock("search_courses", {"subject": "CSCI"}), + _make_text_completion_mock("Here are the results."), + ]) + mock_executor = AsyncMock(return_value=tool_output) + + asyncio.run(gemini.generate_with_tools( + system_prompt="You are helpful.", + user_message="Find CSCI courses", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + second_call_kwargs = gemini.openai_client.chat.completions.create.call_args_list[1][1] + messages = second_call_kwargs["messages"] + + assistant_msg = messages[-2] + self.assertEqual(assistant_msg["role"], "assistant") + + tool_msg = messages[-1] + self.assertEqual(tool_msg["role"], "tool") + self.assertEqual(tool_msg["tool_call_id"], "call_123") + self.assertEqual(json.loads(tool_msg["content"]), tool_output) + + self.assertIn("tools", second_call_kwargs, + "Follow-up call must include tools= so the model can " + "request additional tool calls if needed") + + # ------------------------------------------------------------------ + # Error handling + # ------------------------------------------------------------------ + + def test_tool_executor_failure_serialises_error_and_continues(self, MockSettings, MockCtxMgr): + """If the tool executor raises, the error is serialised as the + tool result and the loop continues to the follow-up completion.""" + gemini = _make_gemini_client(MockSettings, MockCtxMgr) + gemini.openai_client.chat.completions.create = AsyncMock(side_effect=[ + _make_tool_call_mock("search_courses", {"subject": "CSCI"}), + _make_text_completion_mock("Sorry, I couldn't look that up."), + ]) + mock_executor = AsyncMock(side_effect=RuntimeError("network down")) + + result = asyncio.run(gemini.generate_with_tools( + system_prompt="You are helpful.", + user_message="Find CSCI courses", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + self.assertTrue(result.used_tool) + self.assertEqual(result.tool_name, "search_courses") + self.assertEqual(len(result.tool_calls_made), 1) + + second_call_msgs = gemini.openai_client.chat.completions.create.call_args_list[1][1]["messages"] + tool_msg = [m for m in second_call_msgs if m.get("role") == "tool"][0] + self.assertIn("network down", json.loads(tool_msg["content"])["error"]) + + def test_connection_error_returns_not_used(self, MockSettings, MockCtxMgr): + """APIConnectionError during tool calling returns used_tool=False.""" + gemini = _make_gemini_client(MockSettings, MockCtxMgr) + gemini.openai_client.chat.completions.create = AsyncMock( + side_effect=APIConnectionError(request=MagicMock()), + ) + + result = asyncio.run(gemini.generate_with_tools( + system_prompt="Test", + user_message="Hi", + tool_definitions=[FAKE_TOOL], + tool_executor=AsyncMock(), + )) + + self.assertIsInstance(result, ToolCallResult) + self.assertFalse(result.used_tool) + self.assertIn("unable to connect", result.text.lower()) + + def test_status_error_returns_not_used(self, MockSettings, MockCtxMgr): + """APIStatusError during tool calling returns used_tool=False.""" + gemini = _make_gemini_client(MockSettings, MockCtxMgr) + mock_response = MagicMock() + mock_response.status_code = 500 + gemini.openai_client.chat.completions.create = AsyncMock( + side_effect=APIStatusError( + message="Server error", response=mock_response, body=None, + ) + ) + + result = asyncio.run(gemini.generate_with_tools( + system_prompt="Test", + user_message="Hi", + tool_definitions=[FAKE_TOOL], + tool_executor=AsyncMock(), + )) + + self.assertIsInstance(result, ToolCallResult) + self.assertFalse(result.used_tool) + self.assertIn("error", result.text.lower()) + + # ------------------------------------------------------------------ + # Multi-tool call in a single response + # ------------------------------------------------------------------ + + def test_parallel_tool_calls_all_executed(self, MockSettings, MockCtxMgr): + """When the model requests multiple tool calls in one response, + all of them are executed and their results fed back.""" + gemini = _make_gemini_client(MockSettings, MockCtxMgr) + gemini.openai_client.chat.completions.create = AsyncMock(side_effect=[ + _make_multi_tool_call_mock([ + ("rate_my_professor", {"professor_name": "Dubson"}, "call_a"), + ("rate_my_professor", {"professor_name": "West"}, "call_b"), + ]), + _make_text_completion_mock("Dubson has a 4.5 rating. West has a 3.8 rating."), + ]) + mock_executor = AsyncMock(side_effect=[ + {"professors": [{"name": "Dubson", "rating": 4.5}]}, + {"professors": [{"name": "West", "rating": 3.8}]}, + ]) + + result = asyncio.run(gemini.generate_with_tools( + system_prompt="You are helpful.", + user_message="Is professor Dubson or West rated better?", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + self.assertEqual(mock_executor.call_count, 2) + self.assertTrue(result.used_tool) + self.assertIn("Dubson", result.text) + self.assertIn("West", result.text) + self.assertEqual(len(result.tool_calls_made), 2) + self.assertEqual(result.tool_calls_made[0].name, "rate_my_professor") + self.assertEqual(result.tool_calls_made[1].args, {"professor_name": "West"}) + self.assertEqual(gemini.openai_client.chat.completions.create.call_count, 2) + + def test_parallel_tool_results_all_in_followup_messages(self, MockSettings, MockCtxMgr): + """All tool results must appear as separate role:tool messages + in the follow-up request.""" + gemini = _make_gemini_client(MockSettings, MockCtxMgr) + gemini.openai_client.chat.completions.create = AsyncMock(side_effect=[ + _make_multi_tool_call_mock([ + ("rate_my_professor", {"professor_name": "Dubson"}, "call_a"), + ("rate_my_professor", {"professor_name": "West"}, "call_b"), + ]), + _make_text_completion_mock("Comparison complete."), + ]) + mock_executor = AsyncMock(side_effect=[ + {"professors": [{"name": "Dubson"}]}, + {"professors": [{"name": "West"}]}, + ]) + + asyncio.run(gemini.generate_with_tools( + system_prompt="You are helpful.", + user_message="Compare", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + second_call_msgs = gemini.openai_client.chat.completions.create.call_args_list[1][1]["messages"] + tool_msgs = [m for m in second_call_msgs if m.get("role") == "tool"] + self.assertEqual(len(tool_msgs), 2) + self.assertEqual(tool_msgs[0]["tool_call_id"], "call_a") + self.assertEqual(tool_msgs[1]["tool_call_id"], "call_b") + + # ------------------------------------------------------------------ + # Multi-round tool calling + # ------------------------------------------------------------------ + + def test_sequential_tool_rounds(self, MockSettings, MockCtxMgr): + """The loop handles a second round of tool calls after the first + results are fed back.""" + gemini = _make_gemini_client(MockSettings, MockCtxMgr) + gemini.openai_client.chat.completions.create = AsyncMock(side_effect=[ + _make_tool_call_mock("rate_my_professor", {"professor_name": "Dubson"}, "call_1"), + _make_tool_call_mock("rate_my_professor", {"professor_name": "West"}, "call_2"), + _make_text_completion_mock("Dubson is rated higher than West."), + ]) + mock_executor = AsyncMock(side_effect=[ + {"professors": [{"name": "Dubson", "rating": 4.5}]}, + {"professors": [{"name": "West", "rating": 3.8}]}, + ]) + + result = asyncio.run(gemini.generate_with_tools( + system_prompt="You are helpful.", + user_message="Compare Dubson and West", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + self.assertEqual(mock_executor.call_count, 2) + self.assertTrue(result.used_tool) + self.assertEqual(len(result.tool_calls_made), 2) + self.assertEqual(result.tool_name, "rate_my_professor") + self.assertEqual(gemini.openai_client.chat.completions.create.call_count, 3) + + # ------------------------------------------------------------------ + # Partial failure in multi-tool context + # ------------------------------------------------------------------ + + def test_partial_tool_failure_continues(self, MockSettings, MockCtxMgr): + """If one tool call in a batch fails, the error is serialised + and the loop continues to the follow-up.""" + gemini = _make_gemini_client(MockSettings, MockCtxMgr) + gemini.openai_client.chat.completions.create = AsyncMock(side_effect=[ + _make_multi_tool_call_mock([ + ("rate_my_professor", {"professor_name": "Dubson"}, "call_a"), + ("rate_my_professor", {"professor_name": "West"}, "call_b"), + ]), + _make_text_completion_mock("Only Dubson data available."), + ]) + mock_executor = AsyncMock(side_effect=[ + {"professors": [{"name": "Dubson", "rating": 4.5}]}, + RuntimeError("network down"), + ]) + + result = asyncio.run(gemini.generate_with_tools( + system_prompt="You are helpful.", + user_message="Compare", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + self.assertTrue(result.used_tool) + self.assertEqual(len(result.tool_calls_made), 2) + self.assertEqual(gemini.openai_client.chat.completions.create.call_count, 2) + + second_call_msgs = gemini.openai_client.chat.completions.create.call_args_list[1][1]["messages"] + tool_msgs = [m for m in second_call_msgs if m.get("role") == "tool"] + self.assertEqual(len(tool_msgs), 2) + error_content = json.loads(tool_msgs[1]["content"]) + self.assertIn("error", error_content) diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_jwt_secret.py b/multi_llm_chatbot_backend/app/tests/unit/test_jwt_secret.py new file mode 100644 index 0000000000000000000000000000000000000000..989cefdb92d962d640039c03935c8501be6e7e6d --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_jwt_secret.py @@ -0,0 +1,30 @@ +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from app.config import resolve_jwt_secret + + +class TestResolveJwtSecret(unittest.TestCase): + def test_prefers_explicit_config_value(self): + self.assertEqual(resolve_jwt_secret("from-config"), "from-config") + + @patch.dict(os.environ, {"JWT_SECRET_KEY": "from-env"}, clear=False) + def test_uses_env_when_config_empty(self): + self.assertEqual(resolve_jwt_secret(""), "from-env") + + def test_persists_generated_secret_to_data_dir(self): + with tempfile.TemporaryDirectory() as tmp: + with patch.dict(os.environ, {"DATA_DIR": tmp, "JWT_SECRET_KEY": ""}, clear=False): + first = resolve_jwt_secret("") + second = resolve_jwt_secret("") + secret_path = Path(tmp) / "jwt_secret" + self.assertTrue(secret_path.is_file()) + self.assertEqual(first, second) + self.assertGreater(len(first), 20) + + +if __name__ == "__main__": + unittest.main() diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_lucide_icons.py b/multi_llm_chatbot_backend/app/tests/unit/test_lucide_icons.py new file mode 100644 index 0000000000000000000000000000000000000000..47d306bc74363cc8986f1ee1db8caae0d88aeb9c --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_lucide_icons.py @@ -0,0 +1,66 @@ +import unittest +from pydantic import ValidationError +from app.utils.lucide_icons import get_valid_icon_names +from app.config import FeatureConfig, ExampleCategory, PersonaItemConfig + + +class TestIconRegistry(unittest.TestCase): + + def setUp(self): + get_valid_icon_names.cache_clear() + + def tearDown(self): + get_valid_icon_names.cache_clear() + + def test_returns_known_icons(self): + icons = get_valid_icon_names() + self.assertGreater(len(icons), 1500) + self.assertIn("HelpCircle", icons) + self.assertIn("BookOpen", icons) + + def test_rejects_invalid_icon(self): + icons = get_valid_icon_names() + self.assertNotIn("NotARealIcon", icons) + + +class TestIconValidation(unittest.TestCase): + + def setUp(self): + get_valid_icon_names.cache_clear() + + def tearDown(self): + get_valid_icon_names.cache_clear() + + def test_feature_config_accepts_valid_icon(self): + feature = FeatureConfig(title="Test", description="desc", icon="BookOpen") + self.assertEqual(feature.icon, "BookOpen") + + def test_example_category_accepts_valid_icon(self): + example = ExampleCategory(title="Test", icon="Brain") + self.assertEqual(example.icon, "Brain") + + def test_persona_item_accepts_valid_icon(self): + persona = PersonaItemConfig(id="test", name="Test", icon="Heart") + self.assertEqual(persona.icon, "Heart") + + def test_default_icons_are_valid(self): + feature = FeatureConfig(title="Test", description="desc") + self.assertEqual(feature.icon, "HelpCircle") + + example = ExampleCategory(title="Test") + self.assertEqual(example.icon, "BookOpen") + + persona = PersonaItemConfig(id="test", name="Test") + self.assertEqual(persona.icon, "HelpCircle") + + def test_feature_config_rejects_invalid_icon(self): + with self.assertRaisesRegex(ValidationError, "Unknown icon"): + FeatureConfig(title="Test", description="desc", icon="NotARealIcon") + + def test_example_category_rejects_invalid_icon(self): + with self.assertRaisesRegex(ValidationError, "Unknown icon"): + ExampleCategory(title="Test", icon="TotallyFake") + + def test_persona_item_rejects_invalid_icon(self): + with self.assertRaisesRegex(ValidationError, "Unknown icon"): + PersonaItemConfig(id="test", name="Test", icon="Nope") diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_nutrition_calculator.py b/multi_llm_chatbot_backend/app/tests/unit/test_nutrition_calculator.py new file mode 100644 index 0000000000000000000000000000000000000000..1bf810b0fe2f93ee4a9453f6f3ac9f7c039a5c45 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_nutrition_calculator.py @@ -0,0 +1,94 @@ +import unittest + +from app.tools.nutrition_calculator import ( + bodyweight_to_kg, + compute_protein_advisory_context, + conversation_mentions_protein, + extract_weight_from_text, + meal_split, + protein_target_g, +) + + +class TestBodyweightConversion(unittest.TestCase): + def test_230_lbs_to_kg(self): + self.assertEqual(bodyweight_to_kg(230, "lb"), 104.33) + + def test_kg_passthrough(self): + self.assertEqual(bodyweight_to_kg(104.33, "kg"), 104.33) + + def test_unsupported_unit_raises(self): + with self.assertRaises(ValueError): + bodyweight_to_kg(100, "stone") + + +class TestProteinTarget(unittest.TestCase): + def test_230_lbs_protein_range(self): + weight_kg = bodyweight_to_kg(230, "lb") + protein = protein_target_g(weight_kg) + self.assertEqual(weight_kg, 104.33) + self.assertEqual(protein["protein_g_min"], 167) + self.assertEqual(protein["protein_g_mid"], 198) + self.assertEqual(protein["protein_g_max"], 230) + + +class TestMealSplit(unittest.TestCase): + def test_splits_evenly_with_remainder(self): + result = meal_split(167, meals=4) + self.assertEqual(result["per_meal_g"], [42, 42, 42, 41]) + self.assertEqual(sum(result["per_meal_g"]), 167) + + def test_requires_positive_meals(self): + with self.assertRaises(ValueError): + meal_split(100, meals=0) + + +class TestWeightExtraction(unittest.TestCase): + def test_extracts_lbs(self): + info = extract_weight_from_text("I weigh 230 lbs") + self.assertIsNotNone(info) + self.assertEqual(info["value"], 230) + self.assertEqual(info["unit"], "lb") + self.assertEqual(info["weight_kg"], 104.33) + + def test_extracts_kg(self): + info = extract_weight_from_text("about 104.33 kg") + self.assertIsNotNone(info) + self.assertEqual(info["weight_kg"], 104.33) + + def test_standalone_number_defaults_to_lb(self): + info = extract_weight_from_text("230") + self.assertIsNotNone(info) + self.assertEqual(info["unit"], "lb") + + +class TestProteinAdvisoryContext(unittest.TestCase): + def test_computes_from_conversation(self): + messages = [ + {"role": "user", "content": "how much protein should I consume?"}, + {"role": "nutrition_strategist", "content": "What is your body weight?"}, + {"role": "user", "content": "230 lbs"}, + ] + computed = compute_protein_advisory_context(messages) + self.assertIsNotNone(computed) + self.assertEqual(computed["weight_kg"], 104.33) + self.assertEqual(computed["protein_g_min"], 167) + self.assertEqual(computed["protein_g_mid"], 198) + self.assertEqual(computed["protein_g_max"], 230) + self.assertEqual(len(computed["meal_split_min_g"]), 4) + + def test_returns_none_without_protein_topic(self): + messages = [{"role": "user", "content": "230 lbs"}] + self.assertIsNone(compute_protein_advisory_context(messages)) + + def test_returns_none_without_weight(self): + messages = [{"role": "user", "content": "how much protein per day?"}] + self.assertIsNone(compute_protein_advisory_context(messages)) + + def test_conversation_mentions_protein(self): + self.assertTrue(conversation_mentions_protein("How much protein should I eat?")) + self.assertFalse(conversation_mentions_protein("best chest exercises")) + + +if __name__ == "__main__": + unittest.main() diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_nutrition_calculator_tool.py b/multi_llm_chatbot_backend/app/tests/unit/test_nutrition_calculator_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..90c71ee3c0d48ccc9077942bd98f36ffdbde833f --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_nutrition_calculator_tool.py @@ -0,0 +1,25 @@ +import asyncio +import unittest + +from app.tools.nutrition_calculator import TOOL_DEFINITION, execute + + +class TestNutritionCalculatorTool(unittest.TestCase): + def test_tool_definition_shape(self): + self.assertEqual(TOOL_DEFINITION["type"], "function") + self.assertEqual( + TOOL_DEFINITION["function"]["name"], + "calculate_protein_target", + ) + + def test_execute_230_lbs(self): + result = asyncio.run(execute(weight=230, unit="lb", meals=4)) + self.assertEqual(result["weight_kg"], 104.33) + self.assertEqual(result["protein_g_min"], 167) + self.assertEqual(result["protein_g_mid"], 198) + self.assertEqual(result["protein_g_max"], 230) + self.assertEqual(sum(result["meal_split_min_g"]), 167) + + def test_execute_missing_args(self): + result = asyncio.run(execute()) + self.assertIn("error", result) diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_persona_config.py b/multi_llm_chatbot_backend/app/tests/unit/test_persona_config.py new file mode 100644 index 0000000000000000000000000000000000000000..1762bfa10a7257dd2ffecf441934b02b9f5e0efc --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_persona_config.py @@ -0,0 +1,140 @@ +import unittest +import os +import tempfile +import yaml +import app.config +from app.config import load_settings, load_personas_from_dir, PersonasConfig + + +def _write_config(tmp_path, data: dict) -> str: + path = os.path.join(tmp_path, "config.yaml") + with open(path, "w") as f: + yaml.dump(data, f) + return path + + +def _write_persona(directory, filename, data: dict): + path = os.path.join(directory, filename) + with open(path, "w") as f: + yaml.dump(data, f) + + +class TestLoadSettings(unittest.TestCase): + + def setUp(self): + app.config._settings = None + self._tmp = tempfile.TemporaryDirectory() + self.tmp_path = self._tmp.name + + def tearDown(self): + app.config._settings = None + self._tmp.cleanup() + + def test_loads_personas_from_main_config(self): + cfg_path = _write_config(self.tmp_path, { + "personas": { + "base_prompt": "Be helpful.", + "items": [ + {"id": "test1", "name": "Test One", "persona_prompt": "You are test1."}, + ] + } + }) + settings = load_settings(cfg_path) + self.assertEqual(len(settings.personas.items), 1) + self.assertEqual(settings.personas.items[0].id, "test1") + + def test_uses_personas_dir(self): + """Test that load_settings loads personas from a directory when personas_dir is set.""" + personas_dir = os.path.join(self.tmp_path, "personas") + os.makedirs(personas_dir) + + _write_persona(personas_dir, "one.yaml", {"id": "one", "name": "One"}) + _write_persona(personas_dir, "two.yaml", {"id": "two", "name": "Two"}) + + cfg_path = _write_config(self.tmp_path, { + "personas": { + "base_prompt": "Be helpful.", + "personas_dir": "personas", + } + }) + + settings = load_settings(cfg_path) + self.assertEqual(len(settings.personas.items), 2) + ids = {p.id for p in settings.personas.items} + self.assertEqual(ids, {"one", "two"}) + + def test_bad_persona_does_not_crash_everything(self): + """Validates that a bad persona in the inline items list causes a + validation error -- the directory loader solves this for file-based configs.""" + cfg_path = _write_config(self.tmp_path, { + "personas": { + "items": [ + {"id": "good", "name": "Good"}, + {"not_an_id": "bad"}, + ] + } + }) + with self.assertRaises(Exception): + load_settings(cfg_path) + + +class TestLoadPersonasFromDir(unittest.TestCase): + + def setUp(self): + app.config._settings = None + self._tmp = tempfile.TemporaryDirectory() + self.tmp_path = self._tmp.name + + def tearDown(self): + app.config._settings = None + self._tmp.cleanup() + + def test_loads_personas_from_directory(self): + _write_persona(self.tmp_path, "one.yaml", {"id": "one", "name": "One"}) + _write_persona(self.tmp_path, "two.yaml", {"id": "two", "name": "Two"}) + result = load_personas_from_dir(self.tmp_path) + self.assertEqual(len(result), 2) + ids = {p.id for p in result} + self.assertEqual(ids, {"one", "two"}) + + def test_personas_config_validator_loads_from_dir(self): + """Test that PersonasConfig's model_validator loads personas automatically.""" + _write_persona(self.tmp_path, "one.yaml", {"id": "one", "name": "One"}) + _write_persona(self.tmp_path, "two.yaml", {"id": "two", "name": "Two"}) + + config = PersonasConfig(personas_dir=self.tmp_path) + self.assertEqual(len(config.items), 2) + ids = {p.id for p in config.items} + self.assertEqual(ids, {"one", "two"}) + + def test_skips_invalid_persona_files(self): + _write_persona(self.tmp_path, "good.yaml", {"id": "good", "name": "Good"}) + _write_persona(self.tmp_path, "bad.yaml", {"not_id": "bad"}) + result = load_personas_from_dir(self.tmp_path) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].id, "good") + + def test_disabled_persona_excluded(self): + _write_persona(self.tmp_path, "on.yaml", {"id": "on", "name": "On"}) + _write_persona(self.tmp_path, "off.yaml", {"id": "off", "name": "Off", "enabled": False}) + result = load_personas_from_dir(self.tmp_path) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].id, "on") + + def test_duplicate_id_rejected(self): + _write_persona(self.tmp_path, "a.yaml", {"id": "same", "name": "First"}) + _write_persona(self.tmp_path, "b.yaml", {"id": "same", "name": "Second"}) + result = load_personas_from_dir(self.tmp_path) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].name, "First") + + def test_duplicate_name_rejected(self): + _write_persona(self.tmp_path, "a.yaml", {"id": "id_a", "name": "Same Name"}) + _write_persona(self.tmp_path, "b.yaml", {"id": "id_b", "name": "Same Name"}) + result = load_personas_from_dir(self.tmp_path) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].id, "id_a") + + def test_missing_directory_returns_empty(self): + result = load_personas_from_dir(os.path.join(self.tmp_path, "nonexistent")) + self.assertEqual(result, []) diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_resilient_client.py b/multi_llm_chatbot_backend/app/tests/unit/test_resilient_client.py new file mode 100644 index 0000000000000000000000000000000000000000..1a5175c811d9a90e4dd5ea12f18f1788cf3979fb --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_resilient_client.py @@ -0,0 +1,49 @@ +import asyncio +import unittest +from unittest.mock import AsyncMock, MagicMock + +from app.llm.llm_client import ToolCallResult +from app.llm.resilient_client import ResilientLLMClient, _looks_like_failed_response + + +class TestResilientHelpers(unittest.TestCase): + def test_failed_response_detection(self): + self.assertTrue(_looks_like_failed_response("")) + self.assertTrue(_looks_like_failed_response("I'm unable to connect to the AI service.")) + self.assertFalse(_looks_like_failed_response("Hello world")) + + +class TestResilientClient(unittest.TestCase): + def test_primary_failure_uses_fallback(self): + primary = MagicMock() + primary.generate = AsyncMock( + return_value="I'm unable to connect to the AI service. Please ensure the vLLM endpoint is available.", + ) + fallback = MagicMock() + fallback.generate = AsyncMock(return_value="fallback answer") + + client = ResilientLLMClient(primary, fallback, race_timeout_seconds=3.0) + result = asyncio.run( + client.generate("sys", [{"role": "user", "content": "hi"}], 0.5, 100), + ) + self.assertEqual(result, "fallback answer") + fallback.generate.assert_awaited_once() + + def test_race_uses_faster_fallback(self): + async def slow_primary(*_a, **_k): + await asyncio.sleep(5) + return "primary" + + async def fast_fallback(*_a, **_k): + return "fallback fast" + + primary = MagicMock() + primary.generate = slow_primary + fallback = MagicMock() + fallback.generate = fast_fallback + + client = ResilientLLMClient(primary, fallback, race_timeout_seconds=0.05) + result = asyncio.run( + client.generate("sys", [{"role": "user", "content": "hi"}], 0.5, 100), + ) + self.assertEqual(result, "fallback fast") diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_tool_registry.py b/multi_llm_chatbot_backend/app/tests/unit/test_tool_registry.py new file mode 100644 index 0000000000000000000000000000000000000000..f96b5fcb5b86fdeef83dcd0f8ea037510e8c05b7 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_tool_registry.py @@ -0,0 +1,49 @@ +import asyncio +import unittest +from unittest.mock import AsyncMock + +from app.tools import ( + get_tool_definitions, + get_tool_executor, + list_registered_tools, + _REGISTRY, +) + + +KNOWN_TOOLS = {"get_current_datetime", "calculate_protein_target"} + + +class TestToolDiscovery(unittest.TestCase): + def test_known_tools_are_discovered(self): + registered = set(list_registered_tools()) + for name in KNOWN_TOOLS: + self.assertIn(name, registered, f"Tool '{name}' was not discovered") + + def test_registry_entries_have_definition_and_executor(self): + for name, entry in _REGISTRY.items(): + self.assertIn("definition", entry, f"'{name}' missing definition") + self.assertIn("executor", entry, f"'{name}' missing executor") + + +class TestGetToolDefinitions(unittest.TestCase): + def test_returns_all_when_no_filter(self): + defs = get_tool_definitions() + names = {d["function"]["name"] for d in defs} + self.assertTrue(KNOWN_TOOLS.issubset(names)) + + def test_filter_to_single_tool(self): + defs = get_tool_definitions(enabled=["current_datetime"]) + self.assertEqual(len(defs), 1) + self.assertEqual(defs[0]["function"]["name"], "get_current_datetime") + + def test_module_alias_enables_nutrition_tool(self): + defs = get_tool_definitions(enabled=["nutrition_calculator"]) + self.assertEqual(len(defs), 1) + self.assertEqual(defs[0]["function"]["name"], "calculate_protein_target") + + +class TestGetToolExecutor(unittest.TestCase): + def test_dispatch_unknown_tool_returns_error(self): + dispatch = get_tool_executor() + result = asyncio.run(dispatch(name="nonexistent")) + self.assertIn("error", result) diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_version.py b/multi_llm_chatbot_backend/app/tests/unit/test_version.py new file mode 100644 index 0000000000000000000000000000000000000000..6d72dc6bc1a3e5d2cfd9eebce64b9a426f641381 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_version.py @@ -0,0 +1,41 @@ +"""Sanity checks for the centralized ``__version__`` string. + +Guards against regressions where the release automation assumes it can +locate and parse ``app/version.py``. +""" + +import re +import unittest + +from app.api.routes.root import root +from app.version import __version__ + + +VERSION_REGEX = re.compile(r"^\d+\.\d+\.\d+([a-zA-Z0-9.\-+]*)?$") + + +class TestVersion(unittest.TestCase): + + def test_version_string_is_importable_and_well_formed(self): + self.assertIsInstance(__version__, str) + self.assertTrue(__version__) + self.assertRegex( + __version__, + VERSION_REGEX, + f"Unexpected version format: {__version__!r}", + ) + + +class TestRootEndpointVersion(unittest.TestCase): + """The ``/`` handler should surface the same version we imported + from ``app.version``. + + ``app.api.routes.__init__`` eagerly imports every sibling route + module; the heavy ones are replaced with ``MagicMock`` stubs once, + session-wide, in ``conftest.py`` so ``root.py`` can be loaded via + normal import machinery. + """ + + def test_root_endpoint_exposes_imported_version(self): + payload = root() + self.assertEqual(payload["version"], __version__) diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_vllm_client.py b/multi_llm_chatbot_backend/app/tests/unit/test_vllm_client.py new file mode 100644 index 0000000000000000000000000000000000000000..2e76b95e009090f8e43a03198149f240051598a7 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_vllm_client.py @@ -0,0 +1,628 @@ +import asyncio +import json +import unittest +from unittest.mock import AsyncMock, MagicMock, patch + +from openai import APIConnectionError, APIStatusError + +from app.llm.llm_client import ToolCallResult +from app.llm.improved_vllm_client import ImprovedVllmClient + + +FAKE_URL = "https://fake.example.com/vllm0" +FAKE_KEY = "test-key" + +FAKE_TOOL = { + "type": "function", + "function": { + "name": "search_courses", + "description": "Search courses", + "parameters": { + "type": "object", + "properties": { + "subject": {"type": "string", "description": "Subject code"}, + }, + }, + }, +} + + +def _make_completion_mock(content="Response"): + """Build a mock that looks like an OpenAI ChatCompletion.""" + mock_message = MagicMock() + mock_message.content = content + mock_choice = MagicMock() + mock_choice.message = mock_message + return MagicMock(choices=[mock_choice]) + + +def _make_text_completion_mock(content="Response"): + """Build a ChatCompletion mock with no tool calls.""" + mock_message = MagicMock() + mock_message.content = content + mock_message.tool_calls = None + mock_choice = MagicMock() + mock_choice.message = mock_message + return MagicMock(choices=[mock_choice]) + + +def _make_tool_call_mock(fn_name, fn_args_dict, tool_call_id="call_123"): + """Build a ChatCompletion mock where the model requests a tool call.""" + fn_args_json = json.dumps(fn_args_dict) + + tool_call = MagicMock() + tool_call.id = tool_call_id + tool_call.function.name = fn_name + tool_call.function.arguments = fn_args_json + + mock_message = MagicMock() + mock_message.content = None + mock_message.tool_calls = [tool_call] + mock_message.model_dump.return_value = { + "role": "assistant", + "content": None, + "tool_calls": [{ + "id": tool_call_id, + "type": "function", + "function": {"name": fn_name, "arguments": fn_args_json}, + }], + } + + mock_choice = MagicMock() + mock_choice.message = mock_message + return MagicMock(choices=[mock_choice]) + + +def _make_multi_tool_call_mock(calls): + """Build a ChatCompletion mock with multiple parallel tool calls. + + *calls* is a list of (fn_name, fn_args_dict, tool_call_id) tuples. + """ + tool_calls = [] + dump_calls = [] + for fn_name, fn_args_dict, tool_call_id in calls: + fn_args_json = json.dumps(fn_args_dict) + tc = MagicMock() + tc.id = tool_call_id + tc.function.name = fn_name + tc.function.arguments = fn_args_json + tool_calls.append(tc) + dump_calls.append({ + "id": tool_call_id, + "type": "function", + "function": {"name": fn_name, "arguments": fn_args_json}, + }) + + mock_message = MagicMock() + mock_message.content = None + mock_message.tool_calls = tool_calls + mock_message.model_dump.return_value = { + "role": "assistant", + "content": None, + "tool_calls": dump_calls, + } + + mock_choice = MagicMock() + mock_choice.message = mock_message + return MagicMock(choices=[mock_choice]) + + +@patch("app.llm.improved_vllm_client.get_context_manager") +@patch("app.llm.improved_vllm_client.AsyncOpenAI") +class TestImprovedVllmClient(unittest.TestCase): + + # ------------------------------------------------------------------ + # Construction + # ------------------------------------------------------------------ + + def test_constructor_stores_attributes(self, MockAsyncOpenAI, mock_get_ctx): + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + self.assertEqual(client.api_url, FAKE_URL) + self.assertEqual(client.api_key, FAKE_KEY) + self.assertEqual(client.model_name, "test-model") + + def test_constructor_defaults_model_to_none(self, MockAsyncOpenAI, mock_get_ctx): + client = ImprovedVllmClient(api_url=FAKE_URL, api_key=FAKE_KEY) + self.assertIsNone(client.model_name) + + # ------------------------------------------------------------------ + # Model discovery + # ------------------------------------------------------------------ + + def test_refresh_model_discovers_model(self, MockAsyncOpenAI, mock_get_ctx): + client = ImprovedVllmClient(api_url=FAKE_URL, api_key=FAKE_KEY) + + mock_model = MagicMock() + mock_model.id = "discovered-model" + client.client.models.list = AsyncMock( + return_value=MagicMock(data=[mock_model]) + ) + + asyncio.run(client.refresh_model()) + self.assertEqual(client.model_name, "discovered-model") + + # ------------------------------------------------------------------ + # generate โ€“ happy path + # ------------------------------------------------------------------ + + def test_generate_returns_cleaned_response(self, MockAsyncOpenAI, mock_get_ctx): + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock( + return_value=_make_completion_mock(" Here is my response. ") + ) + + result = asyncio.run(client.generate( + system_prompt="You are helpful.", + context=[{"role": "user", "content": "Hello"}], + temperature=0.7, + max_tokens=100, + )) + self.assertEqual(result, "Here is my response.") + + def test_generate_auto_discovers_model_when_none(self, MockAsyncOpenAI, mock_get_ctx): + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name=None, + ) + + mock_model = MagicMock() + mock_model.id = "auto-discovered" + client.client.models.list = AsyncMock( + return_value=MagicMock(data=[mock_model]) + ) + client.client.chat.completions.create = AsyncMock( + return_value=_make_completion_mock() + ) + + asyncio.run(client.generate( + system_prompt="Test", + context=[{"role": "user", "content": "Hi"}], + temperature=0.5, + max_tokens=50, + )) + + client.client.models.list.assert_called_once() + self.assertEqual(client.model_name, "auto-discovered") + + # ------------------------------------------------------------------ + # generate โ€“ error handling + # ------------------------------------------------------------------ + + def test_generate_handles_connection_error(self, MockAsyncOpenAI, mock_get_ctx): + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock( + side_effect=APIConnectionError(request=MagicMock()) + ) + + result = asyncio.run(client.generate( + system_prompt="Test", + context=[{"role": "user", "content": "Hi"}], + temperature=0.5, + max_tokens=50, + )) + self.assertIn("unable to connect", result.lower()) + + def test_generate_handles_status_error(self, MockAsyncOpenAI, mock_get_ctx): + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + mock_response = MagicMock() + mock_response.status_code = 500 + client.client.chat.completions.create = AsyncMock( + side_effect=APIStatusError( + message="Server error", response=mock_response, body=None, + ) + ) + + result = asyncio.run(client.generate( + system_prompt="Test", + context=[{"role": "user", "content": "Hi"}], + temperature=0.5, + max_tokens=50, + )) + self.assertIn("error", result.lower()) + + def test_generate_clears_model_on_404(self, MockAsyncOpenAI, mock_get_ctx): + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="stale-model", + ) + mock_response = MagicMock() + mock_response.status_code = 404 + client.client.chat.completions.create = AsyncMock( + side_effect=APIStatusError( + message="Model not found", response=mock_response, body=None, + ) + ) + + asyncio.run(client.generate( + system_prompt="Test", + context=[{"role": "user", "content": "Hi"}], + temperature=0.5, + max_tokens=50, + )) + self.assertIsNone(client.model_name) + + # ------------------------------------------------------------------ + # generate โ€“ response_format for JSON + # ------------------------------------------------------------------ + + def test_generate_passes_response_format_for_json(self, MockAsyncOpenAI, mock_get_ctx): + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock( + return_value=_make_completion_mock('{"key": "value"}') + ) + + asyncio.run(client.generate( + system_prompt="Return JSON", + context=[{"role": "user", "content": "Hi"}], + temperature=0.3, + max_tokens=100, + response_mime_type="application/json", + )) + + call_kwargs = client.client.chat.completions.create.call_args.kwargs + self.assertEqual(call_kwargs["response_format"], {"type": "json_object"}) + + def test_generate_omits_response_format_when_no_mime_type(self, MockAsyncOpenAI, mock_get_ctx): + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock( + return_value=_make_completion_mock("plain text response") + ) + + asyncio.run(client.generate( + system_prompt="You are helpful.", + context=[{"role": "user", "content": "Hello"}], + temperature=0.7, + max_tokens=100, + )) + + call_kwargs = client.client.chat.completions.create.call_args.kwargs + self.assertNotIn("response_format", call_kwargs) + + # ------------------------------------------------------------------ + # _clean_response + # ------------------------------------------------------------------ + + def test_clean_response_normalizes_whitespace(self, MockAsyncOpenAI, mock_get_ctx): + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + dirty = "Line one.\r\n\r\n\r\n\r\nLine two. " + cleaned = client._clean_response(dirty) + self.assertNotIn("\r", cleaned) + self.assertNotIn("\n\n\n", cleaned) + self.assertEqual(cleaned, "Line one.\n\nLine two.") + + +@patch("app.llm.improved_vllm_client.get_context_manager") +@patch("app.llm.improved_vllm_client.AsyncOpenAI") +class TestVllmGenerateWithTools(unittest.TestCase): + """Unit tests for ImprovedVllmClient.generate_with_tools().""" + + # ------------------------------------------------------------------ + # Happy path โ€” no tool call + # ------------------------------------------------------------------ + + def test_text_response_returns_not_used(self, MockAsyncOpenAI, mock_get_ctx): + """When the model responds with plain text, return used_tool=False.""" + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock( + return_value=_make_text_completion_mock("Hello, world!"), + ) + + result = asyncio.run(client.generate_with_tools( + system_prompt="You are helpful.", + user_message="Hi there", + tool_definitions=[FAKE_TOOL], + tool_executor=AsyncMock(), + )) + + self.assertIsInstance(result, ToolCallResult) + self.assertEqual(result.text, "Hello, world!") + self.assertFalse(result.used_tool) + + # ------------------------------------------------------------------ + # Happy path โ€” tool call + # ------------------------------------------------------------------ + + def test_tool_call_executes_and_returns_final_text(self, MockAsyncOpenAI, mock_get_ctx): + """When the model requests a tool call, execute it and return + the text from the follow-up completion.""" + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock(side_effect=[ + _make_tool_call_mock("search_courses", {"subject": "CSCI"}), + _make_text_completion_mock("CSCI 1300 is available MWF 10-10:50."), + ]) + mock_executor = AsyncMock( + return_value={"courses": [{"title": "Intro to CS"}]}, + ) + + result = asyncio.run(client.generate_with_tools( + system_prompt="You are helpful.", + user_message="What CSCI classes are there?", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + mock_executor.assert_called_once_with( + name="search_courses", subject="CSCI", + ) + self.assertIsInstance(result, ToolCallResult) + self.assertEqual(result.text, "CSCI 1300 is available MWF 10-10:50.") + self.assertTrue(result.used_tool) + self.assertEqual(result.tool_name, "search_courses") + self.assertEqual(result.tool_args, {"subject": "CSCI"}) + self.assertEqual(len(result.tool_calls_made), 1) + self.assertEqual(result.tool_calls_made[0].name, "search_courses") + self.assertEqual(client.client.chat.completions.create.call_count, 2) + + # ------------------------------------------------------------------ + # Payload format + # ------------------------------------------------------------------ + + def test_tool_definitions_passed_through_in_openai_format(self, MockAsyncOpenAI, mock_get_ctx): + """Tool definitions (already in OpenAI format) are passed through + directly to the completions API.""" + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock( + return_value=_make_text_completion_mock("Ok"), + ) + + asyncio.run(client.generate_with_tools( + system_prompt="You are helpful.", + user_message="Hello", + tool_definitions=[FAKE_TOOL], + tool_executor=AsyncMock(), + )) + + call_kwargs = client.client.chat.completions.create.call_args[1] + tools = call_kwargs["tools"] + self.assertEqual(len(tools), 1) + self.assertEqual(tools[0]["type"], "function") + self.assertEqual(tools[0]["function"]["name"], "search_courses") + self.assertIn("parameters", tools[0]["function"]) + + def test_tool_result_appended_to_followup(self, MockAsyncOpenAI, mock_get_ctx): + """After executing a tool, the follow-up call must include + the assistant message, a ``role: tool`` message, and ``tools=``.""" + tool_output = {"courses": [{"title": "Algorithms"}]} + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock(side_effect=[ + _make_tool_call_mock("search_courses", {"subject": "CSCI"}), + _make_text_completion_mock("Here are the results."), + ]) + mock_executor = AsyncMock(return_value=tool_output) + + asyncio.run(client.generate_with_tools( + system_prompt="You are helpful.", + user_message="Find CSCI courses", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + second_call_kwargs = client.client.chat.completions.create.call_args_list[1][1] + messages = second_call_kwargs["messages"] + + assistant_msg = messages[-2] + self.assertEqual(assistant_msg["role"], "assistant") + + tool_msg = messages[-1] + self.assertEqual(tool_msg["role"], "tool") + self.assertEqual(tool_msg["tool_call_id"], "call_123") + self.assertEqual(json.loads(tool_msg["content"]), tool_output) + + self.assertIn("tools", second_call_kwargs, + "Follow-up call must include tools= so the model can " + "request additional tool calls if needed") + + # ------------------------------------------------------------------ + # Error handling + # ------------------------------------------------------------------ + + def test_tool_executor_failure_serialises_error_and_continues(self, MockAsyncOpenAI, mock_get_ctx): + """If the tool executor raises, the error is serialised as the + tool result and the loop continues to the follow-up completion.""" + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock(side_effect=[ + _make_tool_call_mock("search_courses", {"subject": "CSCI"}), + _make_text_completion_mock("Sorry, I couldn't look that up."), + ]) + mock_executor = AsyncMock(side_effect=RuntimeError("network down")) + + result = asyncio.run(client.generate_with_tools( + system_prompt="You are helpful.", + user_message="Find CSCI courses", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + self.assertTrue(result.used_tool) + self.assertEqual(result.tool_name, "search_courses") + self.assertEqual(len(result.tool_calls_made), 1) + + second_call_msgs = client.client.chat.completions.create.call_args_list[1][1]["messages"] + tool_msg = [m for m in second_call_msgs if m.get("role") == "tool"][0] + self.assertIn("network down", json.loads(tool_msg["content"])["error"]) + + def test_connection_error_returns_not_used(self, MockAsyncOpenAI, mock_get_ctx): + """APIConnectionError during tool calling returns used_tool=False.""" + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock( + side_effect=APIConnectionError(request=MagicMock()), + ) + + result = asyncio.run(client.generate_with_tools( + system_prompt="Test", + user_message="Hi", + tool_definitions=[FAKE_TOOL], + tool_executor=AsyncMock(), + )) + + self.assertIsInstance(result, ToolCallResult) + self.assertFalse(result.used_tool) + self.assertIn("unable to connect", result.text.lower()) + + # ------------------------------------------------------------------ + # Multi-tool call in a single response + # ------------------------------------------------------------------ + + def test_parallel_tool_calls_all_executed(self, MockAsyncOpenAI, mock_get_ctx): + """When the model requests multiple tool calls in one response, + all of them are executed and their results fed back.""" + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock(side_effect=[ + _make_multi_tool_call_mock([ + ("rate_my_professor", {"professor_name": "Dubson"}, "call_a"), + ("rate_my_professor", {"professor_name": "West"}, "call_b"), + ]), + _make_text_completion_mock("Dubson has a 4.5 rating. West has a 3.8 rating."), + ]) + mock_executor = AsyncMock( + side_effect=[ + {"professors": [{"name": "Dubson", "rating": 4.5}]}, + {"professors": [{"name": "West", "rating": 3.8}]}, + ], + ) + + result = asyncio.run(client.generate_with_tools( + system_prompt="You are helpful.", + user_message="Is professor Dubson or West rated better?", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + self.assertEqual(mock_executor.call_count, 2) + self.assertTrue(result.used_tool) + self.assertIn("Dubson", result.text) + self.assertIn("West", result.text) + self.assertEqual(len(result.tool_calls_made), 2) + self.assertEqual(result.tool_calls_made[0].name, "rate_my_professor") + self.assertEqual(result.tool_calls_made[1].args, {"professor_name": "West"}) + self.assertEqual(client.client.chat.completions.create.call_count, 2) + + def test_parallel_tool_results_all_in_followup_messages(self, MockAsyncOpenAI, mock_get_ctx): + """All tool results must appear as separate role:tool messages + in the follow-up request.""" + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock(side_effect=[ + _make_multi_tool_call_mock([ + ("rate_my_professor", {"professor_name": "Dubson"}, "call_a"), + ("rate_my_professor", {"professor_name": "West"}, "call_b"), + ]), + _make_text_completion_mock("Comparison complete."), + ]) + mock_executor = AsyncMock(side_effect=[ + {"professors": [{"name": "Dubson"}]}, + {"professors": [{"name": "West"}]}, + ]) + + asyncio.run(client.generate_with_tools( + system_prompt="You are helpful.", + user_message="Compare", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + second_call_msgs = client.client.chat.completions.create.call_args_list[1][1]["messages"] + tool_msgs = [m for m in second_call_msgs if m.get("role") == "tool"] + self.assertEqual(len(tool_msgs), 2) + self.assertEqual(tool_msgs[0]["tool_call_id"], "call_a") + self.assertEqual(tool_msgs[1]["tool_call_id"], "call_b") + + # ------------------------------------------------------------------ + # Multi-round tool calling + # ------------------------------------------------------------------ + + def test_sequential_tool_rounds(self, MockAsyncOpenAI, mock_get_ctx): + """The loop handles a second round of tool calls after the first + results are fed back.""" + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock(side_effect=[ + _make_tool_call_mock("rate_my_professor", {"professor_name": "Dubson"}, "call_1"), + _make_tool_call_mock("rate_my_professor", {"professor_name": "West"}, "call_2"), + _make_text_completion_mock("Dubson is rated higher than West."), + ]) + mock_executor = AsyncMock(side_effect=[ + {"professors": [{"name": "Dubson", "rating": 4.5}]}, + {"professors": [{"name": "West", "rating": 3.8}]}, + ]) + + result = asyncio.run(client.generate_with_tools( + system_prompt="You are helpful.", + user_message="Compare Dubson and West", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + self.assertEqual(mock_executor.call_count, 2) + self.assertTrue(result.used_tool) + self.assertEqual(len(result.tool_calls_made), 2) + self.assertEqual(result.tool_name, "rate_my_professor") + self.assertEqual(client.client.chat.completions.create.call_count, 3) + + # ------------------------------------------------------------------ + # Tool executor failure in multi-tool context + # ------------------------------------------------------------------ + + def test_partial_tool_failure_continues(self, MockAsyncOpenAI, mock_get_ctx): + """If one tool call in a batch fails, the error is serialised + and the loop continues to the follow-up.""" + client = ImprovedVllmClient( + api_url=FAKE_URL, api_key=FAKE_KEY, model_name="test-model", + ) + client.client.chat.completions.create = AsyncMock(side_effect=[ + _make_multi_tool_call_mock([ + ("rate_my_professor", {"professor_name": "Dubson"}, "call_a"), + ("rate_my_professor", {"professor_name": "West"}, "call_b"), + ]), + _make_text_completion_mock("Only Dubson data available."), + ]) + mock_executor = AsyncMock(side_effect=[ + {"professors": [{"name": "Dubson", "rating": 4.5}]}, + RuntimeError("network down"), + ]) + + result = asyncio.run(client.generate_with_tools( + system_prompt="You are helpful.", + user_message="Compare", + tool_definitions=[FAKE_TOOL], + tool_executor=mock_executor, + )) + + self.assertTrue(result.used_tool) + self.assertEqual(len(result.tool_calls_made), 2) + self.assertEqual(client.client.chat.completions.create.call_count, 2) + + second_call_msgs = client.client.chat.completions.create.call_args_list[1][1]["messages"] + tool_msgs = [m for m in second_call_msgs if m.get("role") == "tool"] + self.assertEqual(len(tool_msgs), 2) + error_content = json.loads(tool_msgs[1]["content"]) + self.assertIn("error", error_content) + diff --git a/multi_llm_chatbot_backend/app/tests/unit/test_voice_status.py b/multi_llm_chatbot_backend/app/tests/unit/test_voice_status.py new file mode 100644 index 0000000000000000000000000000000000000000..16de32fa5f6e31a62d6530c94b544ad462508592 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/unit/test_voice_status.py @@ -0,0 +1,189 @@ +import asyncio +import unittest +from datetime import datetime +from unittest.mock import AsyncMock, MagicMock, patch + +import httpx +from bson import ObjectId + +# Heavy modules pulled in transitively by ``app.api.routes.voice`` are +# stubbed once for the whole test session in ``conftest.py``; the import +# below relies on those stubs already being in place. +from app.api.routes.voice import voice_status # noqa: E402 +from app.models.user import User # noqa: E402 + +FAKE_USER_ID = ObjectId() + + +def _make_fake_user(**overrides): + defaults = dict( + _id=FAKE_USER_ID, + firstName="Test", + lastName="User", + email="test@example.com", + hashed_password="$2b$12$fakehash", + is_active=True, + created_at=datetime(2025, 1, 1), + ) + defaults.update(overrides) + return User(**defaults) + + +def _make_async_client(get_side_effect): + """Build a mock httpx.AsyncClient whose `async with` yields a client + with `.get` driven by the supplied side_effect (callable or list).""" + client = MagicMock() + client.get = AsyncMock(side_effect=get_side_effect) + + cm = MagicMock() + cm.__aenter__ = AsyncMock(return_value=client) + cm.__aexit__ = AsyncMock(return_value=None) + return cm, client + + +def _ok_response(): + resp = MagicMock() + resp.status_code = 200 + return resp + + +def _err_response(): + resp = MagicMock() + resp.status_code = 503 + return resp + + +# ------------------------------------------------------------------ +# GET /voice/status +# ------------------------------------------------------------------ + + +class TestVoiceStatus(unittest.TestCase): + + @patch("app.api.routes.voice.STT_BASE", "https://stt.example.com") + @patch("app.api.routes.voice.TTS_BASE", "https://tts.example.com") + def test_both_services_ready(self): + cm, client = _make_async_client([_ok_response(), _ok_response()]) + user = _make_fake_user() + + with patch("app.api.routes.voice.httpx.AsyncClient", return_value=cm): + result = asyncio.run(voice_status(current_user=user)) + + self.assertEqual(result, {"tts_ready": True, "stt_ready": True}) + self.assertEqual(client.get.await_count, 2) + client.get.assert_any_await("https://tts.example.com/status") + client.get.assert_any_await("https://stt.example.com/status") + + @patch("app.api.routes.voice.STT_BASE", "https://stt.example.com") + @patch("app.api.routes.voice.TTS_BASE", "https://tts.example.com") + def test_tts_ready_stt_failure_status(self): + cm, _ = _make_async_client([_ok_response(), _err_response()]) + user = _make_fake_user() + + with patch("app.api.routes.voice.httpx.AsyncClient", return_value=cm): + result = asyncio.run(voice_status(current_user=user)) + + self.assertEqual(result, {"tts_ready": True, "stt_ready": False}) + + @patch("app.api.routes.voice.STT_BASE", "https://stt.example.com") + @patch("app.api.routes.voice.TTS_BASE", "https://tts.example.com") + def test_tts_failure_status_stt_ready(self): + cm, _ = _make_async_client([_err_response(), _ok_response()]) + user = _make_fake_user() + + with patch("app.api.routes.voice.httpx.AsyncClient", return_value=cm): + result = asyncio.run(voice_status(current_user=user)) + + self.assertEqual(result, {"tts_ready": False, "stt_ready": True}) + + @patch("app.api.routes.voice.STT_BASE", "https://stt.example.com") + @patch("app.api.routes.voice.TTS_BASE", "https://tts.example.com") + def test_tts_exception_does_not_break_stt_check(self): + cm, client = _make_async_client( + [httpx.ConnectError("boom"), _ok_response()], + ) + user = _make_fake_user() + + with patch("app.api.routes.voice.httpx.AsyncClient", return_value=cm): + result = asyncio.run(voice_status(current_user=user)) + + self.assertEqual(result, {"tts_ready": False, "stt_ready": True}) + self.assertEqual(client.get.await_count, 2) + + @patch("app.api.routes.voice.STT_BASE", "https://stt.example.com") + @patch("app.api.routes.voice.TTS_BASE", "https://tts.example.com") + def test_stt_exception_keeps_tts_result(self): + cm, _ = _make_async_client( + [_ok_response(), httpx.TimeoutException("slow")], + ) + user = _make_fake_user() + + with patch("app.api.routes.voice.httpx.AsyncClient", return_value=cm): + result = asyncio.run(voice_status(current_user=user)) + + self.assertEqual(result, {"tts_ready": True, "stt_ready": False}) + + @patch("app.api.routes.voice.STT_BASE", "https://stt.example.com") + @patch("app.api.routes.voice.TTS_BASE", "https://tts.example.com") + def test_both_exceptions(self): + cm, _ = _make_async_client( + [httpx.ConnectError("nope"), httpx.ConnectError("nope")], + ) + user = _make_fake_user() + + with patch("app.api.routes.voice.httpx.AsyncClient", return_value=cm): + result = asyncio.run(voice_status(current_user=user)) + + self.assertEqual(result, {"tts_ready": False, "stt_ready": False}) + + @patch("app.api.routes.voice.STT_BASE", "") + @patch("app.api.routes.voice.TTS_BASE", "") + def test_empty_endpoints_skip_http_calls(self): + cm, client = _make_async_client([]) + user = _make_fake_user() + + with patch("app.api.routes.voice.httpx.AsyncClient", return_value=cm): + result = asyncio.run(voice_status(current_user=user)) + + self.assertEqual(result, {"tts_ready": False, "stt_ready": False}) + client.get.assert_not_awaited() + + @patch("app.api.routes.voice.STT_BASE", None) + @patch("app.api.routes.voice.TTS_BASE", None) + def test_none_endpoints_skip_http_calls(self): + cm, client = _make_async_client([]) + user = _make_fake_user() + + with patch("app.api.routes.voice.httpx.AsyncClient", return_value=cm): + result = asyncio.run(voice_status(current_user=user)) + + self.assertEqual(result, {"tts_ready": False, "stt_ready": False}) + client.get.assert_not_awaited() + + @patch("app.api.routes.voice.STT_BASE", "https://stt.example.com") + @patch("app.api.routes.voice.TTS_BASE", "") + def test_only_stt_configured(self): + cm, client = _make_async_client([_ok_response()]) + user = _make_fake_user() + + with patch("app.api.routes.voice.httpx.AsyncClient", return_value=cm): + result = asyncio.run(voice_status(current_user=user)) + + self.assertEqual(result, {"tts_ready": False, "stt_ready": True}) + client.get.assert_awaited_once_with("https://stt.example.com/status") + + @patch("app.api.routes.voice.STT_BASE", "") + @patch("app.api.routes.voice.TTS_BASE", "https://tts.example.com") + def test_only_tts_configured(self): + cm, client = _make_async_client([_ok_response()]) + user = _make_fake_user() + + with patch("app.api.routes.voice.httpx.AsyncClient", return_value=cm): + result = asyncio.run(voice_status(current_user=user)) + + self.assertEqual(result, {"tts_ready": True, "stt_ready": False}) + client.get.assert_awaited_once_with("https://tts.example.com/status") + + +if __name__ == "__main__": + unittest.main() diff --git a/multi_llm_chatbot_backend/app/tools/__init__.py b/multi_llm_chatbot_backend/app/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6616fc41688035fa4b1a0be68cebd46fe92be553 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tools/__init__.py @@ -0,0 +1,128 @@ +""" +Tool registry โ€” auto-discovers tool modules in app.tools and provides +a central API for retrieving definitions and dispatching calls. + +Every tool module in this package must export: + TOOL_DEFINITION : Dict[str, Any] โ€” OpenAI tool format + {"type": "function", "function": {"name": ..., ...}} + execute : async (**kwargs) โ€” returns Dict[str, Any] + +Modules that don't export both are silently skipped. + +Filtering semantics for the ``enabled`` parameter: + None โ€” no filter; all registered tools are available (default) + [] โ€” explicit empty list; no tools are available + [ids] โ€” only the named tools are available +""" + +import importlib +import inspect +import logging +import pkgutil +from typing import Any, Callable, Dict, List, Optional + +logger = logging.getLogger(__name__) + +# Shared User-Agent for HTTP clients in tool modules (FOSE, RMP, etc.). +BROWSER_UA = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/131.0.0.0 Safari/537.36" +) + +_REGISTRY: Dict[str, Dict[str, Any]] = {} + + +def _discover_tools() -> None: + """Scan sibling modules in app.tools and register any that export + TOOL_DEFINITION and execute.""" + import app.tools as tools_pkg + + for _finder, module_name, _is_pkg in pkgutil.iter_modules(tools_pkg.__path__): + qualified = f"app.tools.{module_name}" + try: + mod = importlib.import_module(qualified) + except Exception: + logger.warning("Failed to import tool module: %s", qualified, exc_info=True) + continue + + defn = getattr(mod, "TOOL_DEFINITION", None) + executor = getattr(mod, "execute", None) + + if defn is None or executor is None: + continue + + if (not isinstance(defn, dict) + or defn.get("type") != "function" + or not isinstance(defn.get("function"), dict) + or "name" not in defn["function"]): + logger.warning("Skipping %s: TOOL_DEFINITION not in OpenAI tool format", qualified) + continue + + if not callable(executor) or not inspect.iscoroutinefunction(executor): + logger.warning("Skipping %s: execute is not an async callable", qualified) + continue + + tool_name = defn["function"]["name"] + if tool_name in _REGISTRY: + logger.warning( + "Duplicate tool name '%s' from %s โ€” skipping", tool_name, qualified, + ) + continue + + entry = {"definition": defn, "executor": executor} + _REGISTRY[tool_name] = entry + if module_name != tool_name: + _REGISTRY[module_name] = entry + logger.info("Registered tool: %s (from %s)", tool_name, qualified) + + +def get_tool_definitions(enabled: Optional[List[str]] = None) -> List[Dict[str, Any]]: + """Return OpenAI-format tool dicts for registered tools. + + If *enabled* is provided, only return tools whose names are in that list. + If None, return all registered tools. + """ + if enabled is None: + return [entry["definition"] for entry in _REGISTRY.values()] + + return [ + _REGISTRY[name]["definition"] + for name in enabled + if name in _REGISTRY + ] + + +def get_tool_executor(enabled: Optional[List[str]] = None) -> Callable: + """Return a dispatcher compatible with generate_with_tools(tool_executor=...). + + The returned async callable accepts (name, **kwargs) and routes to the + correct tool executor. If *enabled* is provided, only those tools are + dispatchable. + """ + if enabled is not None: + allowed = {name for name in enabled if name in _REGISTRY} + else: + allowed = None + + async def dispatch(name: str, **kwargs: Any) -> Dict[str, Any]: + if allowed is not None and name not in allowed: + logger.warning("Tool '%s' is not enabled", name) + return {"error": f"Tool not enabled: {name}"} + + entry = _REGISTRY.get(name) + if entry is None: + logger.warning("Unknown tool requested: %s", name) + return {"error": f"Unknown tool: {name}"} + + return await entry["executor"](name=name, **kwargs) + + return dispatch + + +def list_registered_tools() -> List[str]: + """Return the names of all discovered tools.""" + return list(_REGISTRY.keys()) + + +_discover_tools() diff --git a/multi_llm_chatbot_backend/app/tools/current_datetime.py b/multi_llm_chatbot_backend/app/tools/current_datetime.py new file mode 100644 index 0000000000000000000000000000000000000000..cd473200132d3bd60cac777b5275dc4848c4f0ab --- /dev/null +++ b/multi_llm_chatbot_backend/app/tools/current_datetime.py @@ -0,0 +1,62 @@ +"""Return the current date and time for orchestrator context.""" + +from __future__ import annotations + +from datetime import datetime, timezone as dt_timezone +from typing import Any, Dict + +try: + from zoneinfo import ZoneInfo +except ImportError: # pragma: no cover + ZoneInfo = None # type: ignore + +TOOL_DEFINITION = { + "type": "function", + "function": { + "name": "get_current_datetime", + "description": ( + "Get the current date and time in UTC and in a configured local timezone. " + "Use when the user asks about today, deadlines, timelines, schedules, " + "or when accurate temporal context improves the guidance." + ), + "parameters": { + "type": "object", + "properties": { + "timezone": { + "type": "string", + "description": ( + "IANA timezone name (e.g. America/Los_Angeles). " + "Optional; defaults to the app-configured timezone." + ), + }, + }, + "required": [], + }, + }, +} + + +async def execute(name: str = "", timezone: str | None = None, **_: Any) -> Dict[str, Any]: + from app.config import get_settings + + cfg = get_settings().tools.get_tool_config("current_datetime") + tz_name = timezone or cfg.get("default_timezone") or "UTC" + tz = dt_timezone.utc + if ZoneInfo is not None: + try: + tz = ZoneInfo(tz_name) + except Exception: + tz_name = "UTC" + tz = dt_timezone.utc + + now_utc = datetime.now(dt_timezone.utc) + now_local = now_utc.astimezone(tz) + + return { + "utc_iso": now_utc.isoformat(), + "local_iso": now_local.isoformat(), + "local_timezone": tz_name, + "local_weekday": now_local.strftime("%A"), + "local_date": now_local.strftime("%Y-%m-%d"), + "local_time": now_local.strftime("%H:%M:%S"), + } diff --git a/multi_llm_chatbot_backend/app/tools/nutrition_calculator.py b/multi_llm_chatbot_backend/app/tools/nutrition_calculator.py new file mode 100644 index 0000000000000000000000000000000000000000..cdf686ab72ab581567dfc2c0a8e8aecea250f0a8 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tools/nutrition_calculator.py @@ -0,0 +1,242 @@ +"""Deterministic fitness/nutrition calculations for MuscleGrowthAI advisors. + +Core math is local (no API). Optional future integrations: +- USDA FoodData Central (free, API key) โ€” food nutrient lookup +- Open Food Facts (free, no key) โ€” barcode / product lookup +""" + +from __future__ import annotations + +import json +import re +from typing import Any, Dict, List, Optional, Tuple + +_LB_TO_KG = 0.45359237 +_KG_TO_LB = 1 / _LB_TO_KG + +_G_PER_KG_MID = 1.9 # midpoint of 1.6โ€“2.2 g/kg hypertrophy range + +_PROTEIN_TOPIC_KEYWORDS = ( + "protein", + "macro", + "macros", + "g/kg", + "grams of protein", + "how much protein", + "protein intake", + "protein target", + "protein goal", +) + +_WEIGHT_PATTERNS: List[Tuple[str, str]] = [ + (r"(\d+(?:\.\d+)?)\s*(?:lbs?|pounds?)\b", "lb"), + (r"(\d+(?:\.\d+)?)\s*(?:kgs?|kilos?)\b", "kg"), + (r"(\d+(?:\.\d+)?)\s*kg\b", "kg"), + (r"(?:weigh(?:s)?|weight(?:\s+is)?|at)\s+(\d+(?:\.\d+)?)\b", "lb"), + (r"^\s*(\d{2,3}(?:\.\d+)?)\s*$", "lb"), + (r"i['']?m\s+(\d+(?:\.\d+)?)\b", "lb"), + (r"i am\s+(\d+(?:\.\d+)?)\b", "lb"), +] + + +def bodyweight_to_kg(value: float, unit: str) -> float: + """Convert body weight to kilograms.""" + unit_norm = unit.strip().lower() + if unit_norm in {"lb", "lbs", "pound", "pounds"}: + return round(value * _LB_TO_KG, 2) + if unit_norm in {"kg", "kgs", "kilo", "kilos", "kilogram", "kilograms"}: + return round(value, 2) + raise ValueError(f"Unsupported weight unit: {unit!r}") + + +def protein_target_g( + weight_kg: float, + g_per_kg_low: float = 1.6, + g_per_kg_high: float = 2.2, + g_per_kg_mid: float = _G_PER_KG_MID, +) -> Dict[str, int]: + """Return rounded daily protein range in grams for hypertrophy.""" + return { + "protein_g_min": round(weight_kg * g_per_kg_low), + "protein_g_mid": round(weight_kg * g_per_kg_mid), + "protein_g_max": round(weight_kg * g_per_kg_high), + } + + +def meal_split(total_g: int, meals: int = 4) -> Dict[str, Any]: + """Split a daily protein target across meals (integer grams per meal).""" + if meals < 1: + raise ValueError("meals must be at least 1") + base = total_g // meals + remainder = total_g % meals + per_meal = [base + (1 if i < remainder else 0) for i in range(meals)] + return { + "meals": meals, + "total_g": total_g, + "per_meal_g": per_meal, + "per_meal_avg": round(total_g / meals, 1), + } + + +def _plausible_body_weight(value: float, unit: str) -> bool: + """Reject obvious non-body-weight numbers (age, reps, etc.).""" + if unit == "kg": + return 35 <= value <= 250 + return 80 <= value <= 550 + + +def extract_weight_from_text(text: str, default_unit: str = "lb") -> Optional[Dict[str, Any]]: + """Parse the first plausible body-weight mention from *text*.""" + for pattern, unit in _WEIGHT_PATTERNS: + match = re.search(pattern, text, re.IGNORECASE) + if not match: + continue + value = float(match.group(1)) + resolved_unit = unit or default_unit + if not _plausible_body_weight(value, resolved_unit): + continue + weight_kg = bodyweight_to_kg(value, resolved_unit) + return { + "value": value, + "unit": resolved_unit, + "weight_kg": weight_kg, + "source_text": match.group(0).strip(), + } + return None + + +def conversation_mentions_protein(text: str) -> bool: + lowered = text.lower() + return any(keyword in lowered for keyword in _PROTEIN_TOPIC_KEYWORDS) + + +def compute_protein_advisory_context( + messages: List[Dict[str, str]], + *, + g_per_kg_low: float = 1.6, + g_per_kg_high: float = 2.2, + meals: int = 4, +) -> Optional[Dict[str, Any]]: + """When protein is discussed and weight is known, return structured facts.""" + combined_text = "\n".join( + msg.get("content", "") + for msg in messages + if msg.get("role") != "system" + ) + if not conversation_mentions_protein(combined_text): + return None + + weight_info: Optional[Dict[str, Any]] = None + for msg in reversed(messages): + if msg.get("role") == "system": + continue + weight_info = extract_weight_from_text(msg.get("content", "")) + if weight_info: + break + + if not weight_info: + return None + + weight_kg = weight_info["weight_kg"] + protein = protein_target_g(weight_kg, g_per_kg_low, g_per_kg_high) + split_min = meal_split(protein["protein_g_min"], meals=meals) + split_mid = meal_split(protein["protein_g_mid"], meals=meals) + split_max = meal_split(protein["protein_g_max"], meals=meals) + + return { + "topic": "protein", + "weight_input": { + "value": weight_info["value"], + "unit": weight_info["unit"], + "source_text": weight_info["source_text"], + }, + "weight_kg": weight_kg, + "protein_g_min": protein["protein_g_min"], + "protein_g_mid": protein["protein_g_mid"], + "protein_g_max": protein["protein_g_max"], + "g_per_kg_range": [g_per_kg_low, g_per_kg_high], + "g_per_kg_mid": _G_PER_KG_MID, + "meal_split_min_g": split_min["per_meal_g"], + "meal_split_mid_g": split_mid["per_meal_g"], + "meal_split_max_g": split_max["per_meal_g"], + "meals": meals, + "formula": "protein_g = weight_kg ร— g_per_kg (Mifflin-independent; hypertrophy range 1.6โ€“2.2 g/kg)", + } + + +def format_computed_context_block(computed: Dict[str, Any]) -> str: + """Serialize computed values for injection into persona system prompts.""" + return ( + "PRE-COMPUTED NUTRITION VALUES (deterministic Python; use these exact " + "numbers โ€” do not recalculate or guess):\n" + f"{json.dumps(computed, indent=2)}" + ) + + +# --------------------------------------------------------------------------- +# LLM tool surface (optional direct orchestrator path) +# --------------------------------------------------------------------------- + +TOOL_DEFINITION = { + "type": "function", + "function": { + "name": "calculate_protein_target", + "description": ( + "Compute daily protein targets in grams from body weight using the " + "hypertrophy range 1.6โ€“2.2 g/kg. Use when the user asks about protein " + "intake and provides or implies body weight." + ), + "parameters": { + "type": "object", + "properties": { + "weight": { + "type": "number", + "description": "Body weight numeric value.", + }, + "unit": { + "type": "string", + "enum": ["lb", "kg"], + "description": "Unit for weight (lb or kg).", + }, + "meals": { + "type": "integer", + "description": "Number of meals to split protein across (default 4).", + }, + }, + "required": ["weight", "unit"], + }, + }, +} + + +async def execute( + name: str = "", + weight: float | None = None, + unit: str | None = None, + meals: int = 4, + **_: Any, +) -> Dict[str, Any]: + if weight is None or unit is None: + return {"error": "weight and unit are required"} + + try: + weight_kg = bodyweight_to_kg(float(weight), unit) + protein = protein_target_g(weight_kg) + split_min = meal_split(protein["protein_g_min"], meals=max(1, int(meals))) + split_mid = meal_split(protein["protein_g_mid"], meals=max(1, int(meals))) + split_max = meal_split(protein["protein_g_max"], meals=max(1, int(meals))) + except (TypeError, ValueError) as exc: + return {"error": str(exc)} + + return { + "weight_kg": weight_kg, + "protein_g_min": protein["protein_g_min"], + "protein_g_mid": protein["protein_g_mid"], + "protein_g_max": protein["protein_g_max"], + "g_per_kg_range": [1.6, 2.2], + "g_per_kg_mid": _G_PER_KG_MID, + "meal_split_min_g": split_min["per_meal_g"], + "meal_split_mid_g": split_mid["per_meal_g"], + "meal_split_max_g": split_max["per_meal_g"], + "meals": max(1, int(meals)), + } diff --git a/multi_llm_chatbot_backend/app/utils/README.md b/multi_llm_chatbot_backend/app/utils/README.md new file mode 100644 index 0000000000000000000000000000000000000000..615b3f7b6b234fcbfab40b026de6c31e819589a2 --- /dev/null +++ b/multi_llm_chatbot_backend/app/utils/README.md @@ -0,0 +1,142 @@ +# `app/utils` โ€“ Utility Modules for Summarization, Export, and Embeddings + +This directory includes reusable tools that support the backend application with: + +- Chat summarization for display/export +- Document extraction and cleanup +- File export to TXT, DOCX, and PDF formats +- File upload validation +- Persona-specific vector DB with ChromaDB + +These modules are loosely coupled and used across core routes, RAG logic, and export endpoints. + +--- + +## `chat_summary.py` โ€“ Conversation Summarization + +This module provides summarization of past conversations using the LLM client. + +### Key Functions + +- `generate_summary_from_messages(messages, llm, max_tokens)` โ€“ Generates a formatted, bullet-style summary +- `format_summary_for_text_export(summary_text)` โ€“ Cleans summary for export to PDF/DOCX/TXT +- `parse_summary_to_blocks(summary_text)` โ€“ Converts summary to structured blocks (headings, lists, paragraphs) + +### Format Guidelines + +Summaries follow a markdown-style format with: +- `**Section Name:**` for headings +- `* Bullet Points` for insights and recommendations +- Auto-trimming and line breaks for export formatting + +--- + +## `chroma_client.py` โ€“ Persona-Specific Knowledge Store + +A minimal ChromaDB wrapper used to store and query persona-specific documents or embeddings. + +### Functions + +- `add_persona_doc(text, persona, doc_id)` โ€“ Add a new chunk/document for a persona +- `query_persona_knowledge(query, persona)` โ€“ Query ChromaDB for a persona-specific response + +### Notes + +- Uses `./chroma_storage` as the default persistent path +- Uses the local embedding model via `get_embedding()` from `embedding_client.py` + +--- + +## `document_extractor.py` โ€“ File Text Extraction + +Supports extracting raw text from uploaded documents. + +### Supported Formats + +| Format | Content Type | +|--------|---------------| +| PDF | `application/pdf` | +| DOCX | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | +| TXT | `text/plain` | + +### Key Function + +```python +extract_text_from_file(file_bytes: bytes, content_type: str) -> str +``` + +Uses: +- `PyPDF2` for PDFs +- `docx2txt` for Word documents (via temp file) +- UTF-8 decoding for plain text + +--- + +## `file_export.py` โ€“ Export Chat & Summaries + +Exports content (chat logs or summaries) to the following formats: +- `.txt` +- `.docx` (Word) +- `.pdf` (ReportLab) + +### Key Functions + +- `export_chat_as_file(content, format)` โ€“ Unified export method (calls generate_*) +- `prepare_export_response()` โ€“ Returns a `StreamingResponse` with correct content-disposition + +### Formatting Functions + +- `generate_txt_file()` โ€“ Simple UTF-8 stream +- `generate_docx_file()` โ€“ Paragraph-based Word file using `python-docx` +- `generate_pdf_file()` โ€“ Uses ReportLabโ€™s Platypus for chat-style layout +- `generate_pdf_file_from_blocks()` โ€“ Used for structured summaries (heading, lists, etc.) + +All formats apply automatic cleanup and styling via: +- `_clean_text_for_pdf()` and `_render_rich_text()` + +--- + +## `file_limits.py` โ€“ Upload Size Checks + +Used to prevent users from uploading excessively large files in a session. + +### Configurable Limit + +```python +MAX_TOTAL_UPLOAD_MB = 10 +``` + +### Function + +- `is_within_upload_limit(session_id, new_file_bytes, session_context)` โ€“ Returns `True` if upload is within session cap + +Used by routes handling document uploads. + +--- + +## Dependencies + +These modules are used in: + +| Module | Depends On | +|--------|------------| +| `rag_manager.py` | `document_extractor`, `file_limits` | +| `chat_summary.py` | `llm_client` | +| `routes/documents.py` | `document_extractor`, `file_limits` | +| `routes/export.py` | `file_export`, `chat_summary` | + +--- + +## Example Workflow + +```text +Upload File โ†’ document_extractor.py โ†’ raw text + โ†“ + file_limits.py โ†’ check quota + +Chat History โ†’ chat_summary.py โ†’ formatted summary + โ†“ + file_export.py โ†’ TXT, DOCX, PDF + +Persona Notes โ†’ chroma_client.py โ†’ embedded in ChromaDB +``` \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/utils/_lucide_icon_names.json b/multi_llm_chatbot_backend/app/utils/_lucide_icon_names.json new file mode 100644 index 0000000000000000000000000000000000000000..8b43954bbbe4c16a8c25fc4a65400c7cf59d2ecb --- /dev/null +++ b/multi_llm_chatbot_backend/app/utils/_lucide_icon_names.json @@ -0,0 +1,1866 @@ +{ + "version": "0.544.0", + "icons": [ + "AArrowDown", + "AArrowUp", + "ALargeSmall", + "Accessibility", + "Activity", + "ActivitySquare", + "AirVent", + "Airplay", + "AlarmCheck", + "AlarmClock", + "AlarmClockCheck", + "AlarmClockMinus", + "AlarmClockOff", + "AlarmClockPlus", + "AlarmMinus", + "AlarmPlus", + "AlarmSmoke", + "Album", + "AlertCircle", + "AlertOctagon", + "AlertTriangle", + "AlignCenter", + "AlignCenterHorizontal", + "AlignCenterVertical", + "AlignEndHorizontal", + "AlignEndVertical", + "AlignHorizontalDistributeCenter", + "AlignHorizontalDistributeEnd", + "AlignHorizontalDistributeStart", + "AlignHorizontalJustifyCenter", + "AlignHorizontalJustifyEnd", + "AlignHorizontalJustifyStart", + "AlignHorizontalSpaceAround", + "AlignHorizontalSpaceBetween", + "AlignJustify", + "AlignLeft", + "AlignRight", + "AlignStartHorizontal", + "AlignStartVertical", + "AlignVerticalDistributeCenter", + "AlignVerticalDistributeEnd", + "AlignVerticalDistributeStart", + "AlignVerticalJustifyCenter", + "AlignVerticalJustifyEnd", + "AlignVerticalJustifyStart", + "AlignVerticalSpaceAround", + "AlignVerticalSpaceBetween", + "Ambulance", + "Ampersand", + "Ampersands", + "Amphora", + "Anchor", + "Angry", + "Annoyed", + "Antenna", + "Anvil", + "Aperture", + "AppWindow", + "AppWindowMac", + "Apple", + "Archive", + "ArchiveRestore", + "ArchiveX", + "AreaChart", + "Armchair", + "ArrowBigDown", + "ArrowBigDownDash", + "ArrowBigLeft", + "ArrowBigLeftDash", + "ArrowBigRight", + "ArrowBigRightDash", + "ArrowBigUp", + "ArrowBigUpDash", + "ArrowDown", + "ArrowDown01", + "ArrowDown10", + "ArrowDownAZ", + "ArrowDownAz", + "ArrowDownCircle", + "ArrowDownFromLine", + "ArrowDownLeft", + "ArrowDownLeftFromCircle", + "ArrowDownLeftFromSquare", + "ArrowDownLeftSquare", + "ArrowDownNarrowWide", + "ArrowDownRight", + "ArrowDownRightFromCircle", + "ArrowDownRightFromSquare", + "ArrowDownRightSquare", + "ArrowDownSquare", + "ArrowDownToDot", + "ArrowDownToLine", + "ArrowDownUp", + "ArrowDownWideNarrow", + "ArrowDownZA", + "ArrowDownZa", + "ArrowLeft", + "ArrowLeftCircle", + "ArrowLeftFromLine", + "ArrowLeftRight", + "ArrowLeftSquare", + "ArrowLeftToLine", + "ArrowRight", + "ArrowRightCircle", + "ArrowRightFromLine", + "ArrowRightLeft", + "ArrowRightSquare", + "ArrowRightToLine", + "ArrowUp", + "ArrowUp01", + "ArrowUp10", + "ArrowUpAZ", + "ArrowUpAz", + "ArrowUpCircle", + "ArrowUpDown", + "ArrowUpFromDot", + "ArrowUpFromLine", + "ArrowUpLeft", + "ArrowUpLeftFromCircle", + "ArrowUpLeftFromSquare", + "ArrowUpLeftSquare", + "ArrowUpNarrowWide", + "ArrowUpRight", + "ArrowUpRightFromCircle", + "ArrowUpRightFromSquare", + "ArrowUpRightSquare", + "ArrowUpSquare", + "ArrowUpToLine", + "ArrowUpWideNarrow", + "ArrowUpZA", + "ArrowUpZa", + "ArrowsUpFromLine", + "Asterisk", + "AsteriskSquare", + "AtSign", + "Atom", + "AudioLines", + "AudioWaveform", + "Award", + "Axe", + "Axis3D", + "Axis3d", + "Baby", + "Backpack", + "Badge", + "BadgeAlert", + "BadgeCent", + "BadgeCheck", + "BadgeDollarSign", + "BadgeEuro", + "BadgeHelp", + "BadgeIndianRupee", + "BadgeInfo", + "BadgeJapaneseYen", + "BadgeMinus", + "BadgePercent", + "BadgePlus", + "BadgePoundSterling", + "BadgeQuestionMark", + "BadgeRussianRuble", + "BadgeSwissFranc", + "BadgeTurkishLira", + "BadgeX", + "BaggageClaim", + "Ban", + "Banana", + "Bandage", + "Banknote", + "BanknoteArrowDown", + "BanknoteArrowUp", + "BanknoteX", + "BarChart", + "BarChart2", + "BarChart3", + "BarChart4", + "BarChartBig", + "BarChartHorizontal", + "BarChartHorizontalBig", + "Barcode", + "Barrel", + "Baseline", + "Bath", + "Battery", + "BatteryCharging", + "BatteryFull", + "BatteryLow", + "BatteryMedium", + "BatteryPlus", + "BatteryWarning", + "Beaker", + "Bean", + "BeanOff", + "Bed", + "BedDouble", + "BedSingle", + "Beef", + "Beer", + "BeerOff", + "Bell", + "BellDot", + "BellElectric", + "BellMinus", + "BellOff", + "BellPlus", + "BellRing", + "BetweenHorizonalEnd", + "BetweenHorizonalStart", + "BetweenHorizontalEnd", + "BetweenHorizontalStart", + "BetweenVerticalEnd", + "BetweenVerticalStart", + "BicepsFlexed", + "Bike", + "Binary", + "Binoculars", + "Biohazard", + "Bird", + "Bitcoin", + "Blend", + "Blinds", + "Blocks", + "Bluetooth", + "BluetoothConnected", + "BluetoothOff", + "BluetoothSearching", + "Bold", + "Bolt", + "Bomb", + "Bone", + "Book", + "BookA", + "BookAlert", + "BookAudio", + "BookCheck", + "BookCopy", + "BookDashed", + "BookDown", + "BookHeadphones", + "BookHeart", + "BookImage", + "BookKey", + "BookLock", + "BookMarked", + "BookMinus", + "BookOpen", + "BookOpenCheck", + "BookOpenText", + "BookPlus", + "BookTemplate", + "BookText", + "BookType", + "BookUp", + "BookUp2", + "BookUser", + "BookX", + "Bookmark", + "BookmarkCheck", + "BookmarkMinus", + "BookmarkPlus", + "BookmarkX", + "BoomBox", + "Bot", + "BotMessageSquare", + "BotOff", + "BottleWine", + "BowArrow", + "Box", + "BoxSelect", + "Boxes", + "Braces", + "Brackets", + "Brain", + "BrainCircuit", + "BrainCog", + "BrickWall", + "BrickWallFire", + "BrickWallShield", + "Briefcase", + "BriefcaseBusiness", + "BriefcaseConveyorBelt", + "BriefcaseMedical", + "BringToFront", + "Brush", + "BrushCleaning", + "Bubbles", + "Bug", + "BugOff", + "BugPlay", + "Building", + "Building2", + "Bus", + "BusFront", + "Cable", + "CableCar", + "Cake", + "CakeSlice", + "Calculator", + "Calendar", + "Calendar1", + "CalendarArrowDown", + "CalendarArrowUp", + "CalendarCheck", + "CalendarCheck2", + "CalendarClock", + "CalendarCog", + "CalendarDays", + "CalendarFold", + "CalendarHeart", + "CalendarMinus", + "CalendarMinus2", + "CalendarOff", + "CalendarPlus", + "CalendarPlus2", + "CalendarRange", + "CalendarSearch", + "CalendarSync", + "CalendarX", + "CalendarX2", + "Camera", + "CameraOff", + "CandlestickChart", + "Candy", + "CandyCane", + "CandyOff", + "Cannabis", + "Captions", + "CaptionsOff", + "Car", + "CarFront", + "CarTaxiFront", + "Caravan", + "CardSim", + "Carrot", + "CaseLower", + "CaseSensitive", + "CaseUpper", + "CassetteTape", + "Cast", + "Castle", + "Cat", + "Cctv", + "ChartArea", + "ChartBar", + "ChartBarBig", + "ChartBarDecreasing", + "ChartBarIncreasing", + "ChartBarStacked", + "ChartCandlestick", + "ChartColumn", + "ChartColumnBig", + "ChartColumnDecreasing", + "ChartColumnIncreasing", + "ChartColumnStacked", + "ChartGantt", + "ChartLine", + "ChartNetwork", + "ChartNoAxesColumn", + "ChartNoAxesColumnDecreasing", + "ChartNoAxesColumnIncreasing", + "ChartNoAxesCombined", + "ChartNoAxesGantt", + "ChartPie", + "ChartScatter", + "ChartSpline", + "Check", + "CheckCheck", + "CheckCircle", + "CheckCircle2", + "CheckLine", + "CheckSquare", + "CheckSquare2", + "ChefHat", + "Cherry", + "ChevronDown", + "ChevronDownCircle", + "ChevronDownSquare", + "ChevronFirst", + "ChevronLast", + "ChevronLeft", + "ChevronLeftCircle", + "ChevronLeftSquare", + "ChevronRight", + "ChevronRightCircle", + "ChevronRightSquare", + "ChevronUp", + "ChevronUpCircle", + "ChevronUpSquare", + "ChevronsDown", + "ChevronsDownUp", + "ChevronsLeft", + "ChevronsLeftRight", + "ChevronsLeftRightEllipsis", + "ChevronsRight", + "ChevronsRightLeft", + "ChevronsUp", + "ChevronsUpDown", + "Chrome", + "Chromium", + "Church", + "Cigarette", + "CigaretteOff", + "Circle", + "CircleAlert", + "CircleArrowDown", + "CircleArrowLeft", + "CircleArrowOutDownLeft", + "CircleArrowOutDownRight", + "CircleArrowOutUpLeft", + "CircleArrowOutUpRight", + "CircleArrowRight", + "CircleArrowUp", + "CircleCheck", + "CircleCheckBig", + "CircleChevronDown", + "CircleChevronLeft", + "CircleChevronRight", + "CircleChevronUp", + "CircleDashed", + "CircleDivide", + "CircleDollarSign", + "CircleDot", + "CircleDotDashed", + "CircleEllipsis", + "CircleEqual", + "CircleFadingArrowUp", + "CircleFadingPlus", + "CircleGauge", + "CircleHelp", + "CircleMinus", + "CircleOff", + "CircleParking", + "CircleParkingOff", + "CirclePause", + "CirclePercent", + "CirclePlay", + "CirclePlus", + "CirclePoundSterling", + "CirclePower", + "CircleQuestionMark", + "CircleSlash", + "CircleSlash2", + "CircleSlashed", + "CircleSmall", + "CircleStar", + "CircleStop", + "CircleUser", + "CircleUserRound", + "CircleX", + "CircuitBoard", + "Citrus", + "Clapperboard", + "Clipboard", + "ClipboardCheck", + "ClipboardClock", + "ClipboardCopy", + "ClipboardEdit", + "ClipboardList", + "ClipboardMinus", + "ClipboardPaste", + "ClipboardPen", + "ClipboardPenLine", + "ClipboardPlus", + "ClipboardSignature", + "ClipboardType", + "ClipboardX", + "Clock", + "Clock1", + "Clock10", + "Clock11", + "Clock12", + "Clock2", + "Clock3", + "Clock4", + "Clock5", + "Clock6", + "Clock7", + "Clock8", + "Clock9", + "ClockAlert", + "ClockArrowDown", + "ClockArrowUp", + "ClockFading", + "ClockPlus", + "ClosedCaption", + "Cloud", + "CloudAlert", + "CloudCheck", + "CloudCog", + "CloudDownload", + "CloudDrizzle", + "CloudFog", + "CloudHail", + "CloudLightning", + "CloudMoon", + "CloudMoonRain", + "CloudOff", + "CloudRain", + "CloudRainWind", + "CloudSnow", + "CloudSun", + "CloudSunRain", + "CloudUpload", + "Cloudy", + "Clover", + "Club", + "Code", + "Code2", + "CodeSquare", + "CodeXml", + "Codepen", + "Codesandbox", + "Coffee", + "Cog", + "Coins", + "Columns", + "Columns2", + "Columns3", + "Columns3Cog", + "Columns4", + "ColumnsSettings", + "Combine", + "Command", + "Compass", + "Component", + "Computer", + "ConciergeBell", + "Cone", + "Construction", + "Contact", + "Contact2", + "ContactRound", + "Container", + "Contrast", + "Cookie", + "CookingPot", + "Copy", + "CopyCheck", + "CopyMinus", + "CopyPlus", + "CopySlash", + "CopyX", + "Copyleft", + "Copyright", + "CornerDownLeft", + "CornerDownRight", + "CornerLeftDown", + "CornerLeftUp", + "CornerRightDown", + "CornerRightUp", + "CornerUpLeft", + "CornerUpRight", + "Cpu", + "CreativeCommons", + "CreditCard", + "Croissant", + "Crop", + "Cross", + "Crosshair", + "Crown", + "Cuboid", + "CupSoda", + "CurlyBraces", + "Currency", + "Cylinder", + "Dam", + "Database", + "DatabaseBackup", + "DatabaseZap", + "DecimalsArrowLeft", + "DecimalsArrowRight", + "Delete", + "Dessert", + "Diameter", + "Diamond", + "DiamondMinus", + "DiamondPercent", + "DiamondPlus", + "Dice1", + "Dice2", + "Dice3", + "Dice4", + "Dice5", + "Dice6", + "Dices", + "Diff", + "Disc", + "Disc2", + "Disc3", + "DiscAlbum", + "Divide", + "DivideCircle", + "DivideSquare", + "Dna", + "DnaOff", + "Dock", + "Dog", + "DollarSign", + "Donut", + "DoorClosed", + "DoorClosedLocked", + "DoorOpen", + "Dot", + "DotSquare", + "Download", + "DownloadCloud", + "DraftingCompass", + "Drama", + "Dribbble", + "Drill", + "Drone", + "Droplet", + "DropletOff", + "Droplets", + "Drum", + "Drumstick", + "Dumbbell", + "Ear", + "EarOff", + "Earth", + "EarthLock", + "Eclipse", + "Edit", + "Edit2", + "Edit3", + "Egg", + "EggFried", + "EggOff", + "Ellipsis", + "EllipsisVertical", + "Equal", + "EqualApproximately", + "EqualNot", + "EqualSquare", + "Eraser", + "EthernetPort", + "Euro", + "EvCharger", + "Expand", + "ExternalLink", + "Eye", + "EyeClosed", + "EyeOff", + "Facebook", + "Factory", + "Fan", + "FastForward", + "Feather", + "Fence", + "FerrisWheel", + "Figma", + "File", + "FileArchive", + "FileAudio", + "FileAudio2", + "FileAxis3D", + "FileAxis3d", + "FileBadge", + "FileBadge2", + "FileBarChart", + "FileBarChart2", + "FileBox", + "FileChartColumn", + "FileChartColumnIncreasing", + "FileChartLine", + "FileChartPie", + "FileCheck", + "FileCheck2", + "FileClock", + "FileCode", + "FileCode2", + "FileCog", + "FileCog2", + "FileDiff", + "FileDigit", + "FileDown", + "FileEdit", + "FileHeart", + "FileImage", + "FileInput", + "FileJson", + "FileJson2", + "FileKey", + "FileKey2", + "FileLineChart", + "FileLock", + "FileLock2", + "FileMinus", + "FileMinus2", + "FileMusic", + "FileOutput", + "FilePen", + "FilePenLine", + "FilePieChart", + "FilePlay", + "FilePlus", + "FilePlus2", + "FileQuestion", + "FileQuestionMark", + "FileScan", + "FileSearch", + "FileSearch2", + "FileSignature", + "FileSliders", + "FileSpreadsheet", + "FileStack", + "FileSymlink", + "FileTerminal", + "FileText", + "FileType", + "FileType2", + "FileUp", + "FileUser", + "FileVideo", + "FileVideo2", + "FileVideoCamera", + "FileVolume", + "FileVolume2", + "FileWarning", + "FileX", + "FileX2", + "Files", + "Film", + "Filter", + "FilterX", + "Fingerprint", + "FireExtinguisher", + "Fish", + "FishOff", + "FishSymbol", + "Flag", + "FlagOff", + "FlagTriangleLeft", + "FlagTriangleRight", + "Flame", + "FlameKindling", + "Flashlight", + "FlashlightOff", + "FlaskConical", + "FlaskConicalOff", + "FlaskRound", + "FlipHorizontal", + "FlipHorizontal2", + "FlipVertical", + "FlipVertical2", + "Flower", + "Flower2", + "Focus", + "FoldHorizontal", + "FoldVertical", + "Folder", + "FolderArchive", + "FolderCheck", + "FolderClock", + "FolderClosed", + "FolderCode", + "FolderCog", + "FolderCog2", + "FolderDot", + "FolderDown", + "FolderEdit", + "FolderGit", + "FolderGit2", + "FolderHeart", + "FolderInput", + "FolderKanban", + "FolderKey", + "FolderLock", + "FolderMinus", + "FolderOpen", + "FolderOpenDot", + "FolderOutput", + "FolderPen", + "FolderPlus", + "FolderRoot", + "FolderSearch", + "FolderSearch2", + "FolderSymlink", + "FolderSync", + "FolderTree", + "FolderUp", + "FolderX", + "Folders", + "Footprints", + "ForkKnife", + "ForkKnifeCrossed", + "Forklift", + "FormInput", + "Forward", + "Frame", + "Framer", + "Frown", + "Fuel", + "Fullscreen", + "FunctionSquare", + "Funnel", + "FunnelPlus", + "FunnelX", + "GalleryHorizontal", + "GalleryHorizontalEnd", + "GalleryThumbnails", + "GalleryVertical", + "GalleryVerticalEnd", + "Gamepad", + "Gamepad2", + "GanttChart", + "GanttChartSquare", + "Gauge", + "GaugeCircle", + "Gavel", + "Gem", + "GeorgianLari", + "Ghost", + "Gift", + "GitBranch", + "GitBranchPlus", + "GitCommit", + "GitCommitHorizontal", + "GitCommitVertical", + "GitCompare", + "GitCompareArrows", + "GitFork", + "GitGraph", + "GitMerge", + "GitPullRequest", + "GitPullRequestArrow", + "GitPullRequestClosed", + "GitPullRequestCreate", + "GitPullRequestCreateArrow", + "GitPullRequestDraft", + "Github", + "Gitlab", + "GlassWater", + "Glasses", + "Globe", + "Globe2", + "GlobeLock", + "Goal", + "Gpu", + "Grab", + "GraduationCap", + "Grape", + "Grid", + "Grid2X2", + "Grid2X2Check", + "Grid2X2Plus", + "Grid2X2X", + "Grid2x2", + "Grid2x2Check", + "Grid2x2Plus", + "Grid2x2X", + "Grid3X3", + "Grid3x2", + "Grid3x3", + "Grip", + "GripHorizontal", + "GripVertical", + "Group", + "Guitar", + "Ham", + "Hamburger", + "Hammer", + "Hand", + "HandCoins", + "HandFist", + "HandGrab", + "HandHeart", + "HandHelping", + "HandMetal", + "HandPlatter", + "Handbag", + "Handshake", + "HardDrive", + "HardDriveDownload", + "HardDriveUpload", + "HardHat", + "Hash", + "HatGlasses", + "Haze", + "HdmiPort", + "Heading", + "Heading1", + "Heading2", + "Heading3", + "Heading4", + "Heading5", + "Heading6", + "HeadphoneOff", + "Headphones", + "Headset", + "Heart", + "HeartCrack", + "HeartHandshake", + "HeartMinus", + "HeartOff", + "HeartPlus", + "HeartPulse", + "Heater", + "HelpCircle", + "HelpingHand", + "Hexagon", + "Highlighter", + "History", + "Home", + "Hop", + "HopOff", + "Hospital", + "Hotel", + "Hourglass", + "House", + "HouseHeart", + "HousePlug", + "HousePlus", + "HouseWifi", + "IceCream", + "IceCream2", + "IceCreamBowl", + "IceCreamCone", + "IdCard", + "IdCardLanyard", + "Image", + "ImageDown", + "ImageMinus", + "ImageOff", + "ImagePlay", + "ImagePlus", + "ImageUp", + "ImageUpscale", + "Images", + "Import", + "Inbox", + "Indent", + "IndentDecrease", + "IndentIncrease", + "IndianRupee", + "Infinity", + "Info", + "Inspect", + "InspectionPanel", + "Instagram", + "Italic", + "IterationCcw", + "IterationCw", + "JapaneseYen", + "Joystick", + "Kanban", + "KanbanSquare", + "KanbanSquareDashed", + "Kayak", + "Key", + "KeyRound", + "KeySquare", + "Keyboard", + "KeyboardMusic", + "KeyboardOff", + "Lamp", + "LampCeiling", + "LampDesk", + "LampFloor", + "LampWallDown", + "LampWallUp", + "LandPlot", + "Landmark", + "Languages", + "Laptop", + "Laptop2", + "LaptopMinimal", + "LaptopMinimalCheck", + "Lasso", + "LassoSelect", + "Laugh", + "Layers", + "Layers2", + "Layers3", + "Layout", + "LayoutDashboard", + "LayoutGrid", + "LayoutList", + "LayoutPanelLeft", + "LayoutPanelTop", + "LayoutTemplate", + "Leaf", + "LeafyGreen", + "Lectern", + "LetterText", + "Library", + "LibraryBig", + "LibrarySquare", + "LifeBuoy", + "Ligature", + "Lightbulb", + "LightbulbOff", + "LineChart", + "LineSquiggle", + "Link", + "Link2", + "Link2Off", + "Linkedin", + "List", + "ListCheck", + "ListChecks", + "ListChevronsDownUp", + "ListChevronsUpDown", + "ListCollapse", + "ListEnd", + "ListFilter", + "ListFilterPlus", + "ListIndentDecrease", + "ListIndentIncrease", + "ListMinus", + "ListMusic", + "ListOrdered", + "ListPlus", + "ListRestart", + "ListStart", + "ListTodo", + "ListTree", + "ListVideo", + "ListX", + "Loader", + "Loader2", + "LoaderCircle", + "LoaderPinwheel", + "Locate", + "LocateFixed", + "LocateOff", + "LocationEdit", + "Lock", + "LockKeyhole", + "LockKeyholeOpen", + "LockOpen", + "LogIn", + "LogOut", + "Logs", + "Lollipop", + "Luggage", + "MSquare", + "Magnet", + "Mail", + "MailCheck", + "MailMinus", + "MailOpen", + "MailPlus", + "MailQuestion", + "MailQuestionMark", + "MailSearch", + "MailWarning", + "MailX", + "Mailbox", + "Mails", + "Map", + "MapMinus", + "MapPin", + "MapPinCheck", + "MapPinCheckInside", + "MapPinHouse", + "MapPinMinus", + "MapPinMinusInside", + "MapPinOff", + "MapPinPen", + "MapPinPlus", + "MapPinPlusInside", + "MapPinX", + "MapPinXInside", + "MapPinned", + "MapPlus", + "Mars", + "MarsStroke", + "Martini", + "Maximize", + "Maximize2", + "Medal", + "Megaphone", + "MegaphoneOff", + "Meh", + "MemoryStick", + "Menu", + "MenuSquare", + "Merge", + "MessageCircle", + "MessageCircleCode", + "MessageCircleDashed", + "MessageCircleHeart", + "MessageCircleMore", + "MessageCircleOff", + "MessageCirclePlus", + "MessageCircleQuestion", + "MessageCircleQuestionMark", + "MessageCircleReply", + "MessageCircleWarning", + "MessageCircleX", + "MessageSquare", + "MessageSquareCode", + "MessageSquareDashed", + "MessageSquareDiff", + "MessageSquareDot", + "MessageSquareHeart", + "MessageSquareLock", + "MessageSquareMore", + "MessageSquareOff", + "MessageSquarePlus", + "MessageSquareQuote", + "MessageSquareReply", + "MessageSquareShare", + "MessageSquareText", + "MessageSquareWarning", + "MessageSquareX", + "MessagesSquare", + "Mic", + "Mic2", + "MicOff", + "MicVocal", + "Microchip", + "Microscope", + "Microwave", + "Milestone", + "Milk", + "MilkOff", + "Minimize", + "Minimize2", + "Minus", + "MinusCircle", + "MinusSquare", + "Monitor", + "MonitorCheck", + "MonitorCog", + "MonitorDot", + "MonitorDown", + "MonitorOff", + "MonitorPause", + "MonitorPlay", + "MonitorSmartphone", + "MonitorSpeaker", + "MonitorStop", + "MonitorUp", + "MonitorX", + "Moon", + "MoonStar", + "MoreHorizontal", + "MoreVertical", + "Mountain", + "MountainSnow", + "Mouse", + "MouseOff", + "MousePointer", + "MousePointer2", + "MousePointerBan", + "MousePointerClick", + "MousePointerSquareDashed", + "Move", + "Move3D", + "Move3d", + "MoveDiagonal", + "MoveDiagonal2", + "MoveDown", + "MoveDownLeft", + "MoveDownRight", + "MoveHorizontal", + "MoveLeft", + "MoveRight", + "MoveUp", + "MoveUpLeft", + "MoveUpRight", + "MoveVertical", + "Music", + "Music2", + "Music3", + "Music4", + "Navigation", + "Navigation2", + "Navigation2Off", + "NavigationOff", + "Network", + "Newspaper", + "Nfc", + "NonBinary", + "Notebook", + "NotebookPen", + "NotebookTabs", + "NotebookText", + "NotepadText", + "NotepadTextDashed", + "Nut", + "NutOff", + "Octagon", + "OctagonAlert", + "OctagonMinus", + "OctagonPause", + "OctagonX", + "Omega", + "Option", + "Orbit", + "Origami", + "Outdent", + "Package", + "Package2", + "PackageCheck", + "PackageMinus", + "PackageOpen", + "PackagePlus", + "PackageSearch", + "PackageX", + "PaintBucket", + "PaintRoller", + "Paintbrush", + "Paintbrush2", + "PaintbrushVertical", + "Palette", + "Palmtree", + "Panda", + "PanelBottom", + "PanelBottomClose", + "PanelBottomDashed", + "PanelBottomInactive", + "PanelBottomOpen", + "PanelLeft", + "PanelLeftClose", + "PanelLeftDashed", + "PanelLeftInactive", + "PanelLeftOpen", + "PanelLeftRightDashed", + "PanelRight", + "PanelRightClose", + "PanelRightDashed", + "PanelRightInactive", + "PanelRightOpen", + "PanelTop", + "PanelTopBottomDashed", + "PanelTopClose", + "PanelTopDashed", + "PanelTopInactive", + "PanelTopOpen", + "PanelsLeftBottom", + "PanelsLeftRight", + "PanelsRightBottom", + "PanelsTopBottom", + "PanelsTopLeft", + "Paperclip", + "Parentheses", + "ParkingCircle", + "ParkingCircleOff", + "ParkingMeter", + "ParkingSquare", + "ParkingSquareOff", + "PartyPopper", + "Pause", + "PauseCircle", + "PauseOctagon", + "PawPrint", + "PcCase", + "Pen", + "PenBox", + "PenLine", + "PenOff", + "PenSquare", + "PenTool", + "Pencil", + "PencilLine", + "PencilOff", + "PencilRuler", + "Pentagon", + "Percent", + "PercentCircle", + "PercentDiamond", + "PercentSquare", + "PersonStanding", + "PhilippinePeso", + "Phone", + "PhoneCall", + "PhoneForwarded", + "PhoneIncoming", + "PhoneMissed", + "PhoneOff", + "PhoneOutgoing", + "Pi", + "PiSquare", + "Piano", + "Pickaxe", + "PictureInPicture", + "PictureInPicture2", + "PieChart", + "PiggyBank", + "Pilcrow", + "PilcrowLeft", + "PilcrowRight", + "PilcrowSquare", + "Pill", + "PillBottle", + "Pin", + "PinOff", + "Pipette", + "Pizza", + "Plane", + "PlaneLanding", + "PlaneTakeoff", + "Play", + "PlayCircle", + "PlaySquare", + "Plug", + "Plug2", + "PlugZap", + "PlugZap2", + "Plus", + "PlusCircle", + "PlusSquare", + "Pocket", + "PocketKnife", + "Podcast", + "Pointer", + "PointerOff", + "Popcorn", + "Popsicle", + "PoundSterling", + "Power", + "PowerCircle", + "PowerOff", + "PowerSquare", + "Presentation", + "Printer", + "PrinterCheck", + "Projector", + "Proportions", + "Puzzle", + "Pyramid", + "QrCode", + "Quote", + "Rabbit", + "Radar", + "Radiation", + "Radical", + "Radio", + "RadioReceiver", + "RadioTower", + "Radius", + "RailSymbol", + "Rainbow", + "Rat", + "Ratio", + "Receipt", + "ReceiptCent", + "ReceiptEuro", + "ReceiptIndianRupee", + "ReceiptJapaneseYen", + "ReceiptPoundSterling", + "ReceiptRussianRuble", + "ReceiptSwissFranc", + "ReceiptText", + "ReceiptTurkishLira", + "RectangleCircle", + "RectangleEllipsis", + "RectangleGoggles", + "RectangleHorizontal", + "RectangleVertical", + "Recycle", + "Redo", + "Redo2", + "RedoDot", + "RefreshCcw", + "RefreshCcwDot", + "RefreshCw", + "RefreshCwOff", + "Refrigerator", + "Regex", + "RemoveFormatting", + "Repeat", + "Repeat1", + "Repeat2", + "Replace", + "ReplaceAll", + "Reply", + "ReplyAll", + "Rewind", + "Ribbon", + "Rocket", + "RockingChair", + "RollerCoaster", + "Rose", + "Rotate3D", + "Rotate3d", + "RotateCcw", + "RotateCcwKey", + "RotateCcwSquare", + "RotateCw", + "RotateCwSquare", + "Route", + "RouteOff", + "Router", + "Rows", + "Rows2", + "Rows3", + "Rows4", + "Rss", + "Ruler", + "RulerDimensionLine", + "RussianRuble", + "Sailboat", + "Salad", + "Sandwich", + "Satellite", + "SatelliteDish", + "SaudiRiyal", + "Save", + "SaveAll", + "SaveOff", + "Scale", + "Scale3D", + "Scale3d", + "Scaling", + "Scan", + "ScanBarcode", + "ScanEye", + "ScanFace", + "ScanHeart", + "ScanLine", + "ScanQrCode", + "ScanSearch", + "ScanText", + "ScatterChart", + "School", + "School2", + "Scissors", + "ScissorsLineDashed", + "ScissorsSquare", + "ScissorsSquareDashedBottom", + "ScreenShare", + "ScreenShareOff", + "Scroll", + "ScrollText", + "Search", + "SearchCheck", + "SearchCode", + "SearchSlash", + "SearchX", + "Section", + "Send", + "SendHorizonal", + "SendHorizontal", + "SendToBack", + "SeparatorHorizontal", + "SeparatorVertical", + "Server", + "ServerCog", + "ServerCrash", + "ServerOff", + "Settings", + "Settings2", + "Shapes", + "Share", + "Share2", + "Sheet", + "Shell", + "Shield", + "ShieldAlert", + "ShieldBan", + "ShieldCheck", + "ShieldClose", + "ShieldEllipsis", + "ShieldHalf", + "ShieldMinus", + "ShieldOff", + "ShieldPlus", + "ShieldQuestion", + "ShieldQuestionMark", + "ShieldUser", + "ShieldX", + "Ship", + "ShipWheel", + "Shirt", + "ShoppingBag", + "ShoppingBasket", + "ShoppingCart", + "Shovel", + "ShowerHead", + "Shredder", + "Shrimp", + "Shrink", + "Shrub", + "Shuffle", + "Sidebar", + "SidebarClose", + "SidebarOpen", + "Sigma", + "SigmaSquare", + "Signal", + "SignalHigh", + "SignalLow", + "SignalMedium", + "SignalZero", + "Signature", + "Signpost", + "SignpostBig", + "Siren", + "SkipBack", + "SkipForward", + "Skull", + "Slack", + "Slash", + "SlashSquare", + "Slice", + "Sliders", + "SlidersHorizontal", + "SlidersVertical", + "Smartphone", + "SmartphoneCharging", + "SmartphoneNfc", + "Smile", + "SmilePlus", + "Snail", + "Snowflake", + "SoapDispenserDroplet", + "Sofa", + "SortAsc", + "SortDesc", + "Soup", + "Space", + "Spade", + "Sparkle", + "Sparkles", + "Speaker", + "Speech", + "SpellCheck", + "SpellCheck2", + "Spline", + "SplinePointer", + "Split", + "SplitSquareHorizontal", + "SplitSquareVertical", + "Spool", + "Spotlight", + "SprayCan", + "Sprout", + "Square", + "SquareActivity", + "SquareArrowDown", + "SquareArrowDownLeft", + "SquareArrowDownRight", + "SquareArrowLeft", + "SquareArrowOutDownLeft", + "SquareArrowOutDownRight", + "SquareArrowOutUpLeft", + "SquareArrowOutUpRight", + "SquareArrowRight", + "SquareArrowUp", + "SquareArrowUpLeft", + "SquareArrowUpRight", + "SquareAsterisk", + "SquareBottomDashedScissors", + "SquareChartGantt", + "SquareCheck", + "SquareCheckBig", + "SquareChevronDown", + "SquareChevronLeft", + "SquareChevronRight", + "SquareChevronUp", + "SquareCode", + "SquareDashed", + "SquareDashedBottom", + "SquareDashedBottomCode", + "SquareDashedKanban", + "SquareDashedMousePointer", + "SquareDashedTopSolid", + "SquareDivide", + "SquareDot", + "SquareEqual", + "SquareFunction", + "SquareGanttChart", + "SquareKanban", + "SquareLibrary", + "SquareM", + "SquareMenu", + "SquareMinus", + "SquareMousePointer", + "SquareParking", + "SquareParkingOff", + "SquarePause", + "SquarePen", + "SquarePercent", + "SquarePi", + "SquarePilcrow", + "SquarePlay", + "SquarePlus", + "SquarePower", + "SquareRadical", + "SquareRoundCorner", + "SquareScissors", + "SquareSigma", + "SquareSlash", + "SquareSplitHorizontal", + "SquareSplitVertical", + "SquareSquare", + "SquareStack", + "SquareStar", + "SquareStop", + "SquareTerminal", + "SquareUser", + "SquareUserRound", + "SquareX", + "SquaresExclude", + "SquaresIntersect", + "SquaresSubtract", + "SquaresUnite", + "Squircle", + "SquircleDashed", + "Squirrel", + "Stamp", + "Star", + "StarHalf", + "StarOff", + "Stars", + "StepBack", + "StepForward", + "Stethoscope", + "Sticker", + "StickyNote", + "StopCircle", + "Store", + "StretchHorizontal", + "StretchVertical", + "Strikethrough", + "Subscript", + "Subtitles", + "Sun", + "SunDim", + "SunMedium", + "SunMoon", + "SunSnow", + "Sunrise", + "Sunset", + "Superscript", + "SwatchBook", + "SwissFranc", + "SwitchCamera", + "Sword", + "Swords", + "Syringe", + "Table", + "Table2", + "TableCellsMerge", + "TableCellsSplit", + "TableColumnsSplit", + "TableConfig", + "TableOfContents", + "TableProperties", + "TableRowsSplit", + "Tablet", + "TabletSmartphone", + "Tablets", + "Tag", + "Tags", + "Tally1", + "Tally2", + "Tally3", + "Tally4", + "Tally5", + "Tangent", + "Target", + "Telescope", + "Tent", + "TentTree", + "Terminal", + "TerminalSquare", + "TestTube", + "TestTube2", + "TestTubeDiagonal", + "TestTubes", + "Text", + "TextAlignCenter", + "TextAlignEnd", + "TextAlignJustify", + "TextAlignStart", + "TextCursor", + "TextCursorInput", + "TextInitial", + "TextQuote", + "TextSearch", + "TextSelect", + "TextSelection", + "TextWrap", + "Theater", + "Thermometer", + "ThermometerSnowflake", + "ThermometerSun", + "ThumbsDown", + "ThumbsUp", + "Ticket", + "TicketCheck", + "TicketMinus", + "TicketPercent", + "TicketPlus", + "TicketSlash", + "TicketX", + "Tickets", + "TicketsPlane", + "Timer", + "TimerOff", + "TimerReset", + "ToggleLeft", + "ToggleRight", + "Toilet", + "ToolCase", + "Tornado", + "Torus", + "Touchpad", + "TouchpadOff", + "TowerControl", + "ToyBrick", + "Tractor", + "TrafficCone", + "Train", + "TrainFront", + "TrainFrontTunnel", + "TrainTrack", + "TramFront", + "Transgender", + "Trash", + "Trash2", + "TreeDeciduous", + "TreePalm", + "TreePine", + "Trees", + "Trello", + "TrendingDown", + "TrendingUp", + "TrendingUpDown", + "Triangle", + "TriangleAlert", + "TriangleDashed", + "TriangleRight", + "Trophy", + "Truck", + "TruckElectric", + "TurkishLira", + "Turntable", + "Turtle", + "Tv", + "Tv2", + "TvMinimal", + "TvMinimalPlay", + "Twitch", + "Twitter", + "Type", + "TypeOutline", + "Umbrella", + "UmbrellaOff", + "Underline", + "Undo", + "Undo2", + "UndoDot", + "UnfoldHorizontal", + "UnfoldVertical", + "Ungroup", + "University", + "Unlink", + "Unlink2", + "Unlock", + "UnlockKeyhole", + "Unplug", + "Upload", + "UploadCloud", + "Usb", + "User", + "User2", + "UserCheck", + "UserCheck2", + "UserCircle", + "UserCircle2", + "UserCog", + "UserCog2", + "UserLock", + "UserMinus", + "UserMinus2", + "UserPen", + "UserPlus", + "UserPlus2", + "UserRound", + "UserRoundCheck", + "UserRoundCog", + "UserRoundMinus", + "UserRoundPen", + "UserRoundPlus", + "UserRoundSearch", + "UserRoundX", + "UserSearch", + "UserSquare", + "UserSquare2", + "UserStar", + "UserX", + "UserX2", + "Users", + "Users2", + "UsersRound", + "Utensils", + "UtensilsCrossed", + "UtilityPole", + "Variable", + "Vault", + "VectorSquare", + "Vegan", + "VenetianMask", + "Venus", + "VenusAndMars", + "Verified", + "Vibrate", + "VibrateOff", + "Video", + "VideoOff", + "Videotape", + "View", + "Voicemail", + "Volleyball", + "Volume", + "Volume1", + "Volume2", + "VolumeOff", + "VolumeX", + "Vote", + "Wallet", + "Wallet2", + "WalletCards", + "WalletMinimal", + "Wallpaper", + "Wand", + "Wand2", + "WandSparkles", + "Warehouse", + "WashingMachine", + "Watch", + "Waves", + "WavesLadder", + "Waypoints", + "Webcam", + "Webhook", + "WebhookOff", + "Weight", + "Wheat", + "WheatOff", + "WholeWord", + "Wifi", + "WifiCog", + "WifiHigh", + "WifiLow", + "WifiOff", + "WifiPen", + "WifiSync", + "WifiZero", + "Wind", + "WindArrowDown", + "Wine", + "WineOff", + "Workflow", + "Worm", + "WrapText", + "Wrench", + "X", + "XCircle", + "XOctagon", + "XSquare", + "Youtube", + "Zap", + "ZapOff", + "ZoomIn", + "ZoomOut" + ] +} diff --git a/multi_llm_chatbot_backend/app/utils/avatar_helpers.py b/multi_llm_chatbot_backend/app/utils/avatar_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..dca94505464e1a44a399d37fad1b5a762e3d81c7 --- /dev/null +++ b/multi_llm_chatbot_backend/app/utils/avatar_helpers.py @@ -0,0 +1,35 @@ +""" +Bundled avatar image registry. + +Provides helpers for resolving and listing the avatar images shipped in +``app/assets/avatars/``. +""" + +import logging +from functools import lru_cache +from pathlib import Path + +logger = logging.getLogger(__name__) + +_AVATARS_DIR = Path(__file__).resolve().parent.parent / "assets" / "avatars" + + +def get_bundled_avatar_path(name: str) -> Path | None: + """Return the filesystem path for a bundled avatar, or *None* if the + file does not exist.""" + if not _AVATARS_DIR.is_dir(): + return None + candidate = _AVATARS_DIR / name + if candidate.is_file(): + return candidate + return None + + +@lru_cache(maxsize=1) +def list_bundled_avatars() -> tuple[str, ...]: + """Return the filenames of all bundled avatar images.""" + if not _AVATARS_DIR.is_dir(): + return () + avatars = tuple(sorted(f.name for f in _AVATARS_DIR.iterdir() if f.is_file())) + logger.info("Found %d bundled avatar images.", len(avatars)) + return avatars diff --git a/multi_llm_chatbot_backend/app/utils/chat_summary.py b/multi_llm_chatbot_backend/app/utils/chat_summary.py new file mode 100644 index 0000000000000000000000000000000000000000..2c2f5bc6328b7d0dd9498e34a9ea53453f67cfd3 --- /dev/null +++ b/multi_llm_chatbot_backend/app/utils/chat_summary.py @@ -0,0 +1,230 @@ +from typing import Dict, List, Optional + +import logging +import re + +from app.llm.llm_client import LLMClient +from app.config import get_settings + +logger = logging.getLogger(__name__) + +CONTEXT_SUMMARY_SYSTEM = ( + "You are a concise summarizer. Condense the following conversation into a short summary " + "that preserves the key topics discussed, any conclusions reached, important facts shared, " + "and the overall tone. Keep it under 300 words. Write in third person narrative form." +) + + +def _conversation_role_label(role: str, persona_names: Optional[Dict[str, str]] = None) -> str: + if role == "user": + return "User" + if role == "assistant": + return "Assistant" + if persona_names and role in persona_names: + return persona_names[role] + return role.replace("_", " ").title() + + +async def generate_conversation_context_summary( + messages: List[dict], + llm: LLMClient, + persona_names: Optional[Dict[str, str]] = None, + max_tokens: int = 1024, +) -> str: + """Summarize chat history for LLM context when the transcript exceeds the token budget.""" + transcript_lines = [] + for msg in messages: + content = (msg.get("content") or "").strip() + if not content: + continue + label = _conversation_role_label(msg.get("role", "user"), persona_names) + transcript_lines.append(f"{label}: {content}") + + if not transcript_lines: + return "" + + transcript = "\n".join(transcript_lines) + try: + summary = await llm.generate( + system_prompt=CONTEXT_SUMMARY_SYSTEM, + context=[{"role": "user", "content": transcript}], + temperature=0.3, + max_tokens=max_tokens, + ) + return (summary or "").strip() + except Exception as exc: + logger.error("Conversation context summary failed: %s", exc) + return "" + +async def generate_summary_from_messages(messages: List[dict], llm: LLMClient, max_tokens: int = 800) -> str: + """ + Summarize the conversation using the given LLM client. + """ + try: + app_title = get_settings().app.title + full_text = "\n\n".join([f"{m['role']}:\n{m['content']}" for m in messages]) + + system_prompt = ( + f"You are an assistant for {app_title}. Summarize the following chat conversation " + "into a well-formatted summary with clear bullet points. " + "Please format your response as follows:\n" + "- Use bullet points (starting with *) for key insights\n" + "- Put each bullet point on a separate line\n" + "- Include section headings if appropriate (formatted as **Section Name:**)\n" + "- Focus on insights, questions, and actionable advice\n" + "- Maximum 10 bullet points\n\n" + "Example format:\n" + "**Key Insights:**\n" + "* First main point about the conversation\n" + "* Second important insight\n" + "* Third key takeaway\n\n" + "**Recommendations:**\n" + "* First actionable recommendation\n" + "* Second suggestion" + ) + + context = [{"role": "user", "content": f"Chat Log:\n{full_text}"}] + + summary = await llm.generate( + system_prompt=system_prompt, + context=context, + temperature=0.4, + max_tokens=max_tokens + ) + + # Post-process the summary to ensure proper formatting + formatted_summary = _format_summary_text(summary.strip()) + return formatted_summary + + except Exception as e: + logger.error(f"Error generating summary: {str(e)}") + return "Summary generation failed. Please try again later." + + +def _format_summary_text(summary_text: str) -> str: + """ + Post-process the summary text to ensure proper bullet point formatting. + """ + # Fix common formatting issues + + # Add line breaks before bullet points that don't have them + summary_text = re.sub(r'(? List[Dict]: + """ + Parse summary text into structured blocks for better formatting. + """ + # First, ensure proper formatting + summary_text = _format_summary_text(summary_text) + + lines = summary_text.strip().splitlines() + blocks = [] + current_block = None + + def flush_current_block(): + if current_block: + blocks.append(current_block.copy()) + + for line in lines: + line = line.strip() + if not line: + continue + + # Match section headings (e.g. **Title:** or **Title**) + heading_match = re.match(r'^\*\*(.+?)\*\*:?$', line) + if heading_match: + flush_current_block() + current_block = {"type": "heading", "text": heading_match.group(1).strip()} + flush_current_block() + current_block = None + continue + + # Match bullet list items (*, โ€ข, or -) + bullet_match = re.match(r'^[*โ€ข-]\s+(.+)', line) + if bullet_match: + if current_block is None or current_block["type"] != "list" or current_block.get("style") != "bullet": + flush_current_block() + current_block = {"type": "list", "style": "bullet", "items": []} + current_block["items"].append(bullet_match.group(1).strip()) + continue + + # Match numbered list items + number_match = re.match(r'^\d+\.\s+(.+)', line) + if number_match: + if current_block is None or current_block["type"] != "list" or current_block.get("style") != "numbered": + flush_current_block() + current_block = {"type": "list", "style": "numbered", "items": []} + current_block["items"].append(number_match.group(1).strip()) + continue + + # Default: treat as paragraph + flush_current_block() + current_block = {"type": "paragraph", "text": line} + flush_current_block() + current_block = None + + flush_current_block() + + # Debug output to help troubleshoot + logger.info(f"[DEBUG] Parsed {len(blocks)} blocks from summary") + for i, block in enumerate(blocks): + if block["type"] == "list": + logger.info(f"Block {i}: {block['type']} ({block['style']}) with {len(block['items'])} items") + else: + logger.info(f"Block {i}: {block['type']}") + + return blocks + + +def format_summary_for_text_export(summary_text: str) -> str: + """ + Format summary text specifically for TXT and DOCX exports with proper line breaks. + """ + formatted_text = _format_summary_text(summary_text) + + # Add extra spacing for better readability in text formats + lines = formatted_text.split('\n') + formatted_lines = [] + + for line in lines: + line = line.strip() + if not line: + continue + + # Add extra space before section headings + if re.match(r'^\*\*(.+?)\*\*:?$', line): + if formatted_lines: # Don't add space before first heading + formatted_lines.append('') + formatted_lines.append(line) + formatted_lines.append('') # Space after heading + # Add space before bullet points (but group them together) + elif re.match(r'^[*โ€ข-]\s+', line): + # Check if previous line was also a bullet point + if formatted_lines and not re.match(r'^[*โ€ข-]\s+', formatted_lines[-1]): + formatted_lines.append('') # Space before first bullet in group + formatted_lines.append(line) + else: + # Regular paragraph + if formatted_lines: + formatted_lines.append('') + formatted_lines.append(line) + + return '\n'.join(formatted_lines) \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/utils/chroma_client.py b/multi_llm_chatbot_backend/app/utils/chroma_client.py new file mode 100644 index 0000000000000000000000000000000000000000..c9929637d9247a4e0562573af2de9296060c3358 --- /dev/null +++ b/multi_llm_chatbot_backend/app/utils/chroma_client.py @@ -0,0 +1,46 @@ +import os +from pathlib import Path + +import chromadb +from chromadb.config import Settings + +from app.llm.embedding_client import get_embedding + + +def _persona_knowledge_path() -> str: + """Resolve the persona-knowledge ChromaDB path. + + Mirrors ``app.core.rag_manager._default_chroma_path``: when ``DATA_DIR`` + is set (HF Spaces, any bucket-mounted deployment) the embeddings live + under ``${DATA_DIR}/chroma/persona_knowledge`` so they survive Space + rebuilds. Local installs keep the historical relative path. + """ + data_dir = os.environ.get("DATA_DIR", "").strip() + if data_dir: + return str(Path(data_dir) / "chroma" / "persona_knowledge") + return "./chroma_storage" + + +_path = _persona_knowledge_path() +Path(_path).mkdir(parents=True, exist_ok=True) +client = chromadb.PersistentClient(path=_path) + +collection = client.get_or_create_collection("persona_knowledge") + +def add_persona_doc(text: str, persona: str, doc_id: str): + embedding = get_embedding(text) + collection.add( + documents=[text], + embeddings=[embedding], + metadatas=[{"persona": persona}], + ids=[doc_id] + ) + +def query_persona_knowledge(query: str, persona: str): + query_embedding = get_embedding(query) + results = collection.query( + query_embeddings=[query_embedding], + n_results=3, + where={"persona": persona} + ) + return results['documents'][0] if results['documents'] else [] diff --git a/multi_llm_chatbot_backend/app/utils/document_extractor.py b/multi_llm_chatbot_backend/app/utils/document_extractor.py new file mode 100644 index 0000000000000000000000000000000000000000..a644a2c3c279b58e7964ba7b9b63479109803a3f --- /dev/null +++ b/multi_llm_chatbot_backend/app/utils/document_extractor.py @@ -0,0 +1,25 @@ +from io import BytesIO +import PyPDF2 +import tempfile +import docx2txt +import os + +def extract_text_from_file(file_bytes: bytes, content_type: str) -> str: + if content_type == "application/pdf": + reader = PyPDF2.PdfReader(BytesIO(file_bytes)) + return "\n".join(page.extract_text() for page in reader.pages if page.extract_text()) + + elif content_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document": + with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmp: + tmp.write(file_bytes) + tmp_path = tmp.name + try: + return docx2txt.process(tmp_path) + finally: + os.unlink(tmp_path) # Clean up temp file + + elif content_type == "text/plain": + return file_bytes.decode("utf-8") + + else: + raise ValueError("Unsupported file type.") \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/utils/file_export.py b/multi_llm_chatbot_backend/app/utils/file_export.py new file mode 100644 index 0000000000000000000000000000000000000000..c06fd92e18453f093b412ea7a292fb3a986ddea5 --- /dev/null +++ b/multi_llm_chatbot_backend/app/utils/file_export.py @@ -0,0 +1,242 @@ +from io import BytesIO +from typing import List, Tuple, Union +from docx import Document +from reportlab.lib.pagesizes import letter +from reportlab.pdfgen import canvas +from fastapi.responses import StreamingResponse +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, ListFlowable, ListItem +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.enums import TA_CENTER, TA_LEFT +from reportlab.lib.units import inch +from io import BytesIO +import re + +def format_messages_for_export(messages: List[dict]) -> str: + """ + Convert chat messages into a structured exportable string. + """ + return "\n\n".join([f"{m['role']}:\n{m['content'].strip()}" for m in messages]) + + +def generate_txt_file(text: str) -> BytesIO: + buffer = BytesIO() + buffer.write(text.encode("utf-8")) + buffer.seek(0) + return buffer + + +def generate_docx_file(text: str) -> BytesIO: + doc = Document() + for block in text.split("\n\n"): + doc.add_paragraph(block) + doc.add_paragraph("") # spacing + + buffer = BytesIO() + doc.save(buffer) + buffer.seek(0) + return buffer + + +def _clean_text_for_pdf(text: str) -> str: + """ + Clean text for PDF generation to handle special characters and formatting. + """ + # Remove or replace problematic characters + text = text.replace('\u2019', "'") # Smart apostrophe + text = text.replace('\u2018', "'") # Smart apostrophe + text = text.replace('\u201c', '"') # Smart quote + text = text.replace('\u201d', '"') # Smart quote + text = text.replace('\u2013', '-') # En dash + text = text.replace('\u2014', '-') # Em dash + + # Handle markdown-style formatting + text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) # Bold + text = re.sub(r'\*(.+?)\*', r'\1', text) # Italic + + return text + + +def generate_pdf_file(text: str) -> BytesIO: + """ + Improved PDF generation with proper text wrapping and formatting. + """ + buffer = BytesIO() + doc = SimpleDocTemplate( + buffer, + pagesize=letter, + leftMargin=inch, + rightMargin=inch, + topMargin=inch, + bottomMargin=inch + ) + + styles = getSampleStyleSheet() + + # Create custom styles for better formatting + role_style = ParagraphStyle( + name="RoleStyle", + parent=styles["Normal"], + fontSize=12, + fontName="Helvetica-Bold", + spaceAfter=6, + textColor='blue' + ) + + content_style = ParagraphStyle( + name="ContentStyle", + parent=styles["Normal"], + fontSize=10, + fontName="Helvetica", + leading=14, # Line spacing + spaceAfter=12, + leftIndent=20 + ) + + story = [] + + # Split text into message blocks + blocks = text.split("\n\n") + + for block in blocks: + if not block.strip(): + continue + + # Clean the text for PDF + clean_block = _clean_text_for_pdf(block.strip()) + + # Check if this is a role indicator (user:, assistant:, etc.) + lines = clean_block.split('\n', 1) + if len(lines) > 1 and lines[0].strip().endswith(':'): + # This is a role header + role = lines[0].strip() + content = lines[1].strip() if len(lines) > 1 else "" + + # Add role header + story.append(Paragraph(role, role_style)) + + # Add content if it exists + if content: + # Split long content into smaller paragraphs for better formatting + content_paragraphs = content.split('\n') + for para in content_paragraphs: + if para.strip(): + story.append(Paragraph(para.strip(), content_style)) + else: + # Regular content block + story.append(Paragraph(clean_block, content_style)) + + # Add some space between message blocks + story.append(Spacer(1, 12)) + + doc.build(story) + buffer.seek(0) + return buffer + + +def _render_rich_text(text: str) -> str: + """ + Convert markdown-style **bold** to reportlab bold tags. + """ + return re.sub(r'\*\*(.+?)\*\*', r'\1', text) + + +def generate_pdf_file_from_blocks(blocks: List[dict]) -> BytesIO: + buffer = BytesIO() + doc = SimpleDocTemplate(buffer, pagesize=letter) + + styles = getSampleStyleSheet() + + heading_style = ParagraphStyle( + name="Heading", + parent=styles["Heading1"], + alignment=TA_CENTER, + spaceAfter=20 + ) + + paragraph_style = ParagraphStyle( + name="Paragraph", + parent=styles["Normal"], + alignment=TA_LEFT, + leading=14, + spaceAfter=8 + ) + + list_style = ParagraphStyle( + name="List", + parent=styles["Normal"], + leftIndent=20, + leading=14 + ) + + story = [] + + for block in blocks: + if block["type"] == "heading": + story.append(Paragraph(_render_rich_text(block["text"]), heading_style)) + story.append(Spacer(1, 12)) + + elif block["type"] == "paragraph": + story.append(Paragraph(_render_rich_text(block["text"]), paragraph_style)) + + elif block["type"] == "list": + list_items = [ + ListItem(Paragraph(_render_rich_text(item), list_style)) + for item in block["items"] + ] + story.append( + ListFlowable( + list_items, + bulletType="1" if block.get("style") == "numbered" else "bullet", + start="1" if block.get("style") == "numbered" else None, + leftIndent=20 + ) + ) + story.append(Spacer(1, 8)) + + doc.build(story) + buffer.seek(0) + return buffer + + +def export_chat_as_file(content: Union[str, List[dict]], format: str) -> Tuple[BytesIO, str, str]: + """ + Export either a list of chat messages or a summary string to the specified format. + """ + if isinstance(content, list): + text = format_messages_for_export(content) + elif isinstance(content, str): + text = content.strip() + else: + raise ValueError("Unsupported content type") + + if format == "txt": + return generate_txt_file(text), "chat_export.txt", "text/plain" + + elif format == "docx": + return generate_docx_file(text), "chat_export.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + + elif format == "pdf": + return generate_pdf_file(text), "chat_export.pdf", "application/pdf" + + else: + raise ValueError(f"Unsupported export format: {format}") + + +def prepare_export_response( + content: Union[str, List[dict]], + format: str, + filename_prefix: str = "chat_export" +) -> StreamingResponse: + """ + Prepare a StreamingResponse for export, using the given filename prefix. + """ + stream, filename, media_type = export_chat_as_file(content, format) + + # Replace "chat_export" with custom prefix if needed + final_filename = filename.replace("chat_export", filename_prefix) + + return StreamingResponse( + stream, + media_type=media_type, + headers={"Content-Disposition": f"attachment; filename={final_filename}"} + ) \ No newline at end of file diff --git a/multi_llm_chatbot_backend/app/utils/file_limits.py b/multi_llm_chatbot_backend/app/utils/file_limits.py new file mode 100644 index 0000000000000000000000000000000000000000..1edff3d885529baee997ead27a0372154afa30f5 --- /dev/null +++ b/multi_llm_chatbot_backend/app/utils/file_limits.py @@ -0,0 +1,11 @@ +# utils/file_limits.py +from app.core.context import GlobalSessionContext + +MAX_SESSION_UPLOAD_SIZE_MB = 10 + + +MAX_TOTAL_UPLOAD_MB = 10 + +def is_within_upload_limit(session_id: str, new_file_bytes: bytes, session_context: GlobalSessionContext) -> bool: + size_mb = (session_context.total_upload_size + len(new_file_bytes)) / (1024 * 1024) + return size_mb <= MAX_TOTAL_UPLOAD_MB diff --git a/multi_llm_chatbot_backend/app/utils/lucide_icons.py b/multi_llm_chatbot_backend/app/utils/lucide_icons.py new file mode 100644 index 0000000000000000000000000000000000000000..11ed0325c6799334a6502f445cebe25217b8dcf3 --- /dev/null +++ b/multi_llm_chatbot_backend/app/utils/lucide_icons.py @@ -0,0 +1,31 @@ +""" +Lucide icon name registry. + +Reads a pre-generated list of valid PascalCase Lucide icon names from a +bundled JSON file. Regenerate with:: + + python3 scripts/generate_icon_names.py +""" + +import json +import logging +from functools import lru_cache +from pathlib import Path + +logger = logging.getLogger(__name__) + +_ICON_NAMES_JSON = Path(__file__).resolve().parent / "_lucide_icon_names.json" + + +@lru_cache(maxsize=1) +def get_valid_icon_names() -> frozenset[str]: + """Return the set of valid PascalCase Lucide icon names.""" + try: + data = json.loads(_ICON_NAMES_JSON.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + logger.error("Failed to read icon names from %s: %s", _ICON_NAMES_JSON, exc) + return frozenset() + + icons = frozenset(data["icons"]) + logger.info("Loaded %d Lucide icon names (v%s)", len(icons), data.get("version")) + return icons diff --git a/multi_llm_chatbot_backend/app/version.py b/multi_llm_chatbot_backend/app/version.py new file mode 100644 index 0000000000000000000000000000000000000000..11716805d6277a7d45533879a326ae4456445570 --- /dev/null +++ b/multi_llm_chatbot_backend/app/version.py @@ -0,0 +1,4 @@ +__version__ = "2.0.1a3" + +if __name__ == "__main__": + print(__version__) diff --git a/multi_llm_chatbot_backend/requirements.txt b/multi_llm_chatbot_backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ab5e4b1baf5aede9660bd1d61c2c53cbb7f4b70a --- /dev/null +++ b/multi_llm_chatbot_backend/requirements.txt @@ -0,0 +1,47 @@ +# Core FastAPI framework +fastapi~=0.135 +uvicorn[standard]~=0.44 +python-multipart~=0.0 + +# HTTP client for LLM APIs +httpx~=0.28 +openai~=2.30 +huggingface-hub~=0.36 + +# Document processing +PyPDF2~=3.0 +docx2txt~=0.9 +python-docx~=1.2 + +# Environment configuration +python-dotenv~=1.2 +pyyaml~=6.0 + +# Persona color generation +colorhash~=2.3 + +# Vector database and embeddings +chromadb~=1.5 +sentence-transformers~=5.3 + +# Natural language processing +nltk~=3.9 +tiktoken~=0.12 +markdown~=3.0 + +# PDF generation and export +reportlab~=4.4 + +# Persistence: SQLite via aiosqlite on the HF Storage Bucket mount (no MongoDB). +# pymongo is kept *only* for bson.ObjectId and the DuplicateKeyError / +# PyMongoError types that the existing routers and the SQLite shim raise. +aiosqlite~=0.20 +pymongo~=4.16 + +# Authentication and security +passlib[bcrypt]~=1.7 +bcrypt~=4.3 +python-jose[cryptography]~=3.5 + +# Data validation (required for EmailStr in Pydantic) +email-validator~=2.3 diff --git a/multi_llm_chatbot_backend/test_requirements.txt b/multi_llm_chatbot_backend/test_requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..bc96c658e9a80a1ef21322ca32c4fff66ef6ad49 --- /dev/null +++ b/multi_llm_chatbot_backend/test_requirements.txt @@ -0,0 +1,2 @@ +# Unit testing +pytest \ No newline at end of file diff --git a/personas/career_advisors/application_scheduler.yaml b/personas/career_advisors/application_scheduler.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0fc7a0daf924e8fe0353ad490b325d5de4cec814 --- /dev/null +++ b/personas/career_advisors/application_scheduler.yaml @@ -0,0 +1,19 @@ +id: application_scheduler +name: "Application Scheduler" +role: "Cadence, Deadlines & Tracking" +summary: "Organized & motivating" +icon: "CalendarRange" +temperature: 4 +persona_prompt: | + You are an application scheduler who turns internship search into a realistic weekly system with cadence, deadlines, and tracking. + + **YOUR EXPERTISE:** + - Sustainable daily/weekly application targets based on course load + - Spreadsheet or Kanban tracking (wishlist โ†’ applied โ†’ interview โ†’ offer) + - Prioritizing deadlines, rolling apps, and career-fair follow-ups + - Avoiding burnout while staying consistent through a semester + + **YOUR RESPONSE STYLE:** + - Output a clear weekly plan with day-by-day actions when asked + - Suggest simple tracking columns the student can copy + - Adjust volume for midterms, exams, and travel โ€” quality over vanity metrics diff --git a/personas/career_advisors/career_path_mentor.yaml b/personas/career_advisors/career_path_mentor.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4a29913426e01dba511e3c1ff719db1eb254c242 --- /dev/null +++ b/personas/career_advisors/career_path_mentor.yaml @@ -0,0 +1,19 @@ +id: career_path_mentor +name: "Career Path Mentor" +role: "Networking, Pivot & Full-Time Transition" +summary: "Strategic & encouraging" +icon: "GraduationCap" +temperature: 5 +persona_prompt: | + You are a career path mentor who helps undergraduates network, pivot between internship and full-time paths, and build long-term momentum. + + **YOUR EXPERTISE:** + - Alumni and recruiter outreach on LinkedIn (warm, concise messages) + - Deciding when to prioritize internships vs. entry-level full-time (junior vs. senior) + - Converting class projects, clubs, and part-time work into career narrative + - Negotiation basics for internship and new-grad offers (respectful, realistic) + + **YOUR RESPONSE STYLE:** + - Connect short-term applications to a 1โ€“2 year career story + - Draft outreach messages the student can personalize + - Encourage use of campus career services and alumni networks without overpromising outcomes diff --git a/personas/career_advisors/internship_search_strategist.yaml b/personas/career_advisors/internship_search_strategist.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cecd180a1394d1ad81896041cc3a8c02993e5514 --- /dev/null +++ b/personas/career_advisors/internship_search_strategist.yaml @@ -0,0 +1,19 @@ +id: internship_search_strategist +name: "Internship Search Strategist" +role: "Roles, Platforms & Target Lists" +summary: "Targeted & practical" +icon: "Search" +temperature: 4 +persona_prompt: | + You are an internship search strategist who helps undergraduates build focused target lists and use Handshake, LinkedIn, and company career pages effectively. + + **YOUR EXPERTISE:** + - Translating majors/interests into searchable role titles and keywords + - Handshake filters, LinkedIn Easy Apply vs. company portals, career-fair pipelines + - Building weekly target lists by industry, location, and competitiveness + - Spotting red flags (unpaid scams, vague postings) and prioritizing quality applications + + **YOUR RESPONSE STYLE:** + - Give concrete search queries, filters, and example role titles + - Recommend a short target list (companies + roles), not endless scrolling + - Ask about major, location flexibility, and start term when details are missing diff --git a/personas/career_advisors/interview_coach.yaml b/personas/career_advisors/interview_coach.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6a85301043caffabe83136758b3ee041ef88e2fc --- /dev/null +++ b/personas/career_advisors/interview_coach.yaml @@ -0,0 +1,19 @@ +id: interview_coach +name: "Interview Coach" +role: "Prep, Presence & Company Research" +summary: "Confident & coaching" +icon: "MessagesSquare" +temperature: 5 +persona_prompt: | + You are an interview coach who prepares undergraduates for behavioral, technical-lite, and recruiter screens โ€” virtual or in person. + + **YOUR EXPERTISE:** + - STAR-method behavioral answers and likely questions by industry/role + - Company research checklists (product, news, values, recent earnings/news) + - Virtual presence, body language, and professional attire baselines + - Thank-you notes and post-interview follow-ups + + **YOUR RESPONSE STYLE:** + - Provide sample Q&A the student can practice aloud + - Give a short research plan for the night before an interview + - Push for specificity tied to the student's real experiences diff --git a/personas/career_advisors/resume_optimizer.yaml b/personas/career_advisors/resume_optimizer.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d085d65447d83e2083ef3e16cb8ce34d4e52c048 --- /dev/null +++ b/personas/career_advisors/resume_optimizer.yaml @@ -0,0 +1,19 @@ +id: resume_optimizer +name: "Resume Optimizer" +role: "ATS, Bullets & Skills Match" +summary: "Precise & results-focused" +icon: "FilePenLine" +temperature: 3 +persona_prompt: | + You are a resume optimizer who helps undergraduates rewrite materials so they clear ATS screens and impress recruiters. + + **YOUR EXPERTISE:** + - Strong accomplishment bullets (action + context + metric when possible) + - Keyword alignment to internship and entry-level job descriptions + - ATS-friendly formatting (simple layout, clear section headers, no fragile tables) + - Tailoring one master resume into role-specific versions without inventing experience + + **YOUR RESPONSE STYLE:** + - Rewrite bullets in ready-to-paste form when the user shares content + - Call out missing metrics or vague verbs and suggest how to improve them honestly + - Never fabricate jobs, GPAs, or skills the user did not claim diff --git a/phd-advisor-frontend/.gitignore b/phd-advisor-frontend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..4d29575de80483b005c29bfcac5061cd2f45313e --- /dev/null +++ b/phd-advisor-frontend/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/phd-advisor-frontend/package-lock.json b/phd-advisor-frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..3c286a1ddc5e2de5eef4a1c6d888c479721f4cba --- /dev/null +++ b/phd-advisor-frontend/package-lock.json @@ -0,0 +1,19925 @@ +{ + "name": "phd-advisor-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "phd-advisor-frontend", + "version": "0.1.0", + "dependencies": { + "@codemirror/legacy-modes": "^6.5.2", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^13.5.0", + "@uiw/react-codemirror": "^4.25.9", + "katex": "^0.16.45", + "latex.js": "^0.12.6", + "lucide-react": "^0.544.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-markdown": "^10.1.0", + "react-scripts": "5.0.1", + "rehype-katex": "^7.0.1", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "web-vitals": "^2.1.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.27.5.tgz", + "integrity": "sha512-HLkYQfRICudzcOtjGwkPvGc5nF1b4ljLZh1IRDj50lRZ718NAKVgQpIAUX8bfg6u/yuSKY3L7E0YzIV+OxrB8Q==", + "license": "MIT", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", + "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", + "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.27.1.tgz", + "integrity": "sha512-DTxe4LBPrtFdsWzgpmbBKevg3e9PBy+dXRt19kSbucbZvL2uqtdqwwpluL1jfxYE0wIDTFp1nTy/q6gNLsxXrg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-decorators": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", + "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", + "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", + "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.5.tgz", + "integrity": "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", + "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", + "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", + "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz", + "integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.3", + "@babel/plugin-transform-parameters": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", + "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", + "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.27.1.tgz", + "integrity": "sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.5.tgz", + "integrity": "sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.27.4.tgz", + "integrity": "sha512-D68nR5zxU64EUzV8i7T3R5XP0Xhrou/amNnddsRQssx6GrTLdZl1rLxyjtVZBd+v/NVX4AbTPOB5aU8thAZV1A==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz", + "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", + "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", + "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.27.1", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.4.tgz", + "integrity": "sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", + "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "license": "MIT" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz", + "integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", + "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz", + "integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.42.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.42.1.tgz", + "integrity": "sha512-ToN3oFc0nsxNUYVF5P0ztLgbC4UPPjPtA9aKYhkOKQaZASpOUo6ISXyQLP66ctVwlDc+j6Jv0uK5IFALkiXztg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@csstools/normalize.css": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz", + "integrity": "sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ==", + "license": "CC0-1.0" + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", + "license": "CC0-1.0", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", + "license": "CC0-1.0", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "license": "MIT", + "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/eslintrc/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==", + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/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==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/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==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.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==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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==", + "license": "MIT", + "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/@isaacs/cliui/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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==", + "license": "MIT", + "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/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", + "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/core": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", + "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", + "license": "MIT", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/reporters": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^27.5.1", + "jest-config": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-resolve-dependencies": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "jest-watcher": "^27.5.1", + "micromatch": "^4.0.4", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", + "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", + "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", + "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/types": "^27.5.1", + "expect": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", + "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-haste-map": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", + "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9", + "source-map": "^0.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/test-result": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", + "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", + "license": "MIT", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", + "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", + "license": "MIT", + "dependencies": { + "@jest/test-result": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-runtime": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", + "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.5.1", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-util": "^27.5.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/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==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.16.tgz", + "integrity": "sha512-kLQc9xz6QIqd2oIYyXRUiAp79kGpFBm3fEM9ahfG1HI0WI5gdZ2OVHWdmZYnwODt7ISck+QuQ6sBPrtvUBML7Q==", + "license": "MIT", + "dependencies": { + "ansi-html": "^0.0.9", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <5.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz", + "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", + "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", + "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", + "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", + "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", + "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", + "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", + "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", + "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", + "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", + "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", + "@svgr/babel-plugin-transform-svg-component": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", + "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", + "license": "MIT", + "dependencies": { + "@svgr/plugin-jsx": "^5.5.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", + "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.12.6" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", + "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.12.3", + "@svgr/babel-preset": "^5.5.0", + "@svgr/hast-util-to-babel-ast": "^5.5.0", + "svg-parser": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", + "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^7.0.0", + "deepmerge": "^4.2.2", + "svgo": "^1.2.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/webpack": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", + "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/plugin-transform-react-constant-elements": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.5", + "@svgr/core": "^5.5.0", + "@svgr/plugin-jsx": "^5.5.0", + "@svgr/plugin-svgo": "^5.5.0", + "loader-utils": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", + "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.12", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", + "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz", + "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==", + "license": "MIT", + "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": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", + "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "license": "MIT" + }, + "node_modules/@types/q": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", + "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "16.0.9", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", + "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", + "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/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==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.9.tgz", + "integrity": "sha512-QFAqr+pu6lDmNpAlecODcF49TlsrZ0bj15zPzfhiqSDl+Um3EsDLFLppixC7kFLn+rdDM2LTvVjn5CPvefpRgw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.9.tgz", + "integrity": "sha512-HftqCBUYShAOH0pGi1CHP8vfm5L8fQ3+0j0VI6lQD6QpK+UBu3J7nxfEN5O/BXMilMNf9ZyFJRvRcuMMOLHMng==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.9", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "Apache-2.0", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "Apache-2.0" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "license": "BSD-3-Clause" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "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==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.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==", + "license": "MIT", + "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/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", + "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==", + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "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==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "license": "MIT", + "dependencies": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "license": "MIT", + "dependencies": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.reduce": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.8.tgz", + "integrity": "sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-array-method-boxes-properly": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "is-string": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "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==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", + "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", + "license": "MIT", + "dependencies": { + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-loader": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz", + "integrity": "sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==", + "license": "MIT", + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.4", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", + "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-named-asset-import": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", + "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", + "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.4", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", + "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.4" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==", + "license": "MIT" + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", + "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^27.5.1", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-react-app": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.1.0.tgz", + "integrity": "sha512-f9B1xMdnkCIqe+2dHrJsoQFRz7reChaAHE/65SdaykPklQqhme2WaC08oD3is77x9ff98/9EazAKFDZv5rFEQg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/plugin-proposal-class-properties": "^7.16.0", + "@babel/plugin-proposal-decorators": "^7.16.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", + "@babel/plugin-proposal-numeric-separator": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@babel/plugin-proposal-private-methods": "^7.16.0", + "@babel/plugin-proposal-private-property-in-object": "^7.16.7", + "@babel/plugin-transform-flow-strip-types": "^7.16.0", + "@babel/plugin-transform-react-display-name": "^7.16.0", + "@babel/plugin-transform-runtime": "^7.16.4", + "@babel/preset-env": "^7.16.4", + "@babel/preset-react": "^7.16.0", + "@babel/preset-typescript": "^7.16.0", + "@babel/runtime": "^7.16.3", + "babel-plugin-macros": "^3.1.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "license": "MIT" + }, + "node_modules/bfj": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz", + "integrity": "sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw==", + "license": "MIT", + "dependencies": { + "bluebird": "^3.7.2", + "check-types": "^11.2.3", + "hoopy": "^0.1.4", + "jsonpath": "^1.1.1", + "tryer": "^1.0.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "license": "BSD-2-Clause" + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "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.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001721", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", + "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/case-sensitive-paths-webpack-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "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/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-types": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", + "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "license": "MIT", + "dependencies": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/coa/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/coa/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==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/coa/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==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/coa/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==", + "license": "MIT" + }, + "node_modules/coa/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==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/coa/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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/coa/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==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "license": "MIT" + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", + "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", + "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-blank-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-declaration-sorter": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", + "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-has-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", + "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", + "license": "MIT", + "dependencies": { + "cssnano": "^5.0.6", + "jest-worker": "^27.0.2", + "postcss": "^8.3.5", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "license": "CC0-1.0", + "bin": { + "css-prefers-color-scheme": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "license": "MIT" + }, + "node_modules/cssdb": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", + "integrity": "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "CC0-1.0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", + "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^5.2.14", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.14", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", + "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "license": "MIT", + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.1", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.4", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.2", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "license": "MIT", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, + "node_modules/csso/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT", + "peer": true + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "license": "BSD-2-Clause" + }, + "node_modules/data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "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==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "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==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "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==", + "license": "MIT", + "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/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "license": "MIT", + "dependencies": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "bin": { + "detect": "bin/detect-port", + "detect-port": "bin/detect-port" + }, + "engines": { + "node": ">= 4.2.1" + } + }, + "node_modules/detect-port-alt/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/detect-port-alt/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "license": "MIT" + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "deprecated": "Use your platform's native DOMException instead", + "license": "MIT", + "dependencies": { + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "BSD-2-Clause" + }, + "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==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.165", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz", + "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==", + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", + "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "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.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "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.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "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==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "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.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.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" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "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==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@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-config-react-app": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", + "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.3", + "@rushstack/eslint-patch": "^1.1.0", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "babel-preset-react-app": "^10.0.1", + "confusing-browser-globals": "^1.0.11", + "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.3.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-testing-library": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "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==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-flowtype": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", + "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", + "license": "BSD-3-Clause", + "dependencies": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@babel/plugin-syntax-flow": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.9", + "eslint": "^8.1.0" + } + }, + "node_modules/eslint-plugin-import": { + "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==", + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "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 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", + "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/experimental-utils": "^5.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "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.9", + "object.fromentries": "^2.0.8", + "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.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-testing-library": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz", + "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.58.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", + "license": "MIT", + "dependencies": { + "@types/eslint": "^7.29.0 || ^8.4.1", + "jest-worker": "^28.0.2", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/jest-worker": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", + "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/eslint-webpack-plugin/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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/eslint/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==", + "license": "Python-2.0" + }, + "node_modules/eslint/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==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/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==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "BSD-2-Clause", + "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/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "license": "BSD-3-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "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.7.1", + "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.12", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/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==", + "license": "MIT", + "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/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filesize": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 0.4.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==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "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/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "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.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/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==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", + "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=10", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "eslint": ">= 6", + "typescript": ">= 2.7", + "vue-template-compiler": "*", + "webpack": ">= 4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/form-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.3.tgz", + "integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "license": "Unlicense" + }, + "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==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "license": "MIT", + "dependencies": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "license": "ISC" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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==", + "license": "BSD-2-Clause" + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "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==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "license": "MIT" + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "license": "MIT" + }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "license": "(Apache-2.0 OR MPL-1.1)" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/hast-util-from-html/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/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==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^1.0.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", + "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyphenation.de": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/hyphenation.de/-/hyphenation.de-0.2.1.tgz", + "integrity": "sha512-s6Y4TFA8xWjRLneOPI6HV/+wzfm2c2yurTvFaXlznmsbeI6waZhMpxu94fSXGNGsrPxrzI1zTtYDEWeEeaANnw==", + "dependencies": { + "hypher": "*" + } + }, + "node_modules/hyphenation.en-us": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/hyphenation.en-us/-/hyphenation.en-us-0.2.1.tgz", + "integrity": "sha512-ItXYgvIpfN8rfXl/GTBQC7DsSb5PPsKh9gGzViK/iWzCS5mvjDebFJ6xCcIYo8dal+nSp2rUzvTT7BosrKlL8A==", + "dependencies": { + "hypher": "*" + } + }, + "node_modules/hypher": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/hypher/-/hypher-0.2.5.tgz", + "integrity": "sha512-kUTpuyzBWWDO2VakmjHC/cxesg4lKQP+Fdc+7lrK4yvjNjkV9vm5UTZMDAwOyyHTOpbkYrAMlNZHG61NnE9vYQ==", + "license": "BSD-3-Clause" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "license": "MIT", + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "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==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-array-buffer": { + "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==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", + "dependencies": { + "async-function": "^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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "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==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "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==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, + "node_modules/is-regex": { + "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==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "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==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "license": "MIT", + "dependencies": { + "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": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", + "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "license": "MIT", + "dependencies": { + "@jest/core": "^27.5.1", + "import-local": "^3.0.2", + "jest-cli": "^27.5.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", + "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "execa": "^5.0.0", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", + "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-cli": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", + "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", + "license": "MIT", + "dependencies": { + "@jest/core": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", + "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.8.0", + "@jest/test-sequencer": "^27.5.1", + "@jest/types": "^27.5.1", + "babel-jest": "^27.5.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.9", + "jest-circus": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-jasmine2": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", + "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", + "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", + "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", + "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", + "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^27.5.1", + "jest-serializer": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-jasmine2": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", + "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", + "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", + "license": "MIT", + "dependencies": { + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", + "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", + "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", + "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-snapshot": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runner": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", + "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", + "license": "MIT", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-leak-detector": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "source-map-support": "^0.5.6", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", + "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/globals": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-serializer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", + "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", + "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.5.1", + "semver": "^7.3.2" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-validate": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", + "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "leven": "^3.1.0", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-watch-typeahead": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz", + "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "jest-regex-util": "^28.0.0", + "jest-watcher": "^28.0.0", + "slash": "^4.0.0", + "string-length": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "jest": "^27.0.0 || ^28.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/console": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", + "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/console/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/test-result": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", + "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", + "license": "MIT", + "dependencies": { + "@jest/console": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/types": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", + "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/emittery": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-message-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", + "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^28.1.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-message-util/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-regex-util": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", + "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", + "license": "MIT", + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", + "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", + "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", + "license": "MIT", + "dependencies": { + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "jest-util": "^28.1.3", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/jest-watch-typeahead/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", + "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", + "license": "MIT", + "dependencies": { + "char-regex": "^2.0.0", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length/node_modules/char-regex": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz", + "integrity": "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/jest-watch-typeahead/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/jest-watcher": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", + "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", + "license": "MIT", + "dependencies": { + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^27.5.1", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.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==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-beautify": { + "version": "1.14.11", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.11.tgz", + "integrity": "sha512-rPogWqAfoYh1Ryqqh2agUpVfbxAhbjuN1SmU86dskQUKouRiggUTCO4+2ym9UPXllc2WAp0J+T5qxn7Um3lCdw==", + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.3", + "glob": "^10.3.3", + "nopt": "^7.2.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "license": "MIT", + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/jsonpath/node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/katex": { + "version": "0.16.45", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", + "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/latex.js": { + "version": "0.12.6", + "resolved": "https://registry.npmjs.org/latex.js/-/latex.js-0.12.6.tgz", + "integrity": "sha512-spMTeSq9cP4vidMQuPgoYGKmsQTMElZhDtNF3NyLIRc03XkuuPvMA0tzb0ShS/otaIJrB7DIHDk0GL3knCisEw==", + "license": "MIT", + "dependencies": { + "commander": "8.x", + "fs-extra": "10.x", + "hyphenation.de": "*", + "hyphenation.en-us": "*", + "js-beautify": "1.14.x", + "stdin": "*", + "svgdom": "^0.1.8" + }, + "bin": { + "latex.js": "bin/latex.js" + }, + "engines": { + "node": ">= 14.0" + } + }, + "node_modules/launch-editor": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", + "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", + "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "license": "CC0-1.0" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz", + "integrity": "sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A==", + "license": "MIT", + "dependencies": { + "array.prototype.reduce": "^1.0.6", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "gopd": "^1.0.1", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "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==", + "license": "MIT", + "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": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-browser-comments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", + "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", + "license": "CC0-1.0", + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "browserslist": ">=4", + "postcss": ">=8" + } + }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-custom-properties": { + "version": "12.1.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-flexbugs-fixes": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.1.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", + "license": "CC0-1.0", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "license": "CC0-1.0", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "license": "MIT", + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-normalize": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", + "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/normalize.css": "*", + "postcss-browser-comments": "^4", + "sanitize.css": "*" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "browserslist": ">= 4", + "postcss": ">= 8" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "license": "MIT", + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "license": "MIT", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-preset-env": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", + "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-cascade-layers": "^1.1.1", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.13", + "browserslist": "^4.21.4", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.1.0", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.10", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/postcss-svgo/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/postcss-svgo/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, + "node_modules/postcss-svgo/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss-svgo/node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/raw-body/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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-app-polyfill": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", + "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", + "license": "MIT", + "dependencies": { + "core-js": "^3.19.2", + "object-assign": "^4.1.1", + "promise": "^8.1.0", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.9", + "whatwg-fetch": "^3.6.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", + "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.0", + "address": "^1.1.2", + "browserslist": "^4.18.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "escape-string-regexp": "^4.0.0", + "filesize": "^8.0.6", + "find-up": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "global-modules": "^2.0.0", + "globby": "^11.0.4", + "gzip-size": "^6.0.0", + "immer": "^9.0.7", + "is-root": "^2.1.0", + "loader-utils": "^3.2.0", + "open": "^8.4.0", + "pkg-up": "^3.1.0", + "prompts": "^2.4.2", + "react-error-overlay": "^6.0.11", + "recursive-readdir": "^2.2.2", + "shell-quote": "^1.7.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils/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==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/react-dev-utils/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==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/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==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/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==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-error-overlay": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz", + "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==", + "license": "MIT" + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", + "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-scripts": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", + "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.16.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", + "@svgr/webpack": "^5.5.0", + "babel-jest": "^27.4.2", + "babel-loader": "^8.2.3", + "babel-plugin-named-asset-import": "^0.3.8", + "babel-preset-react-app": "^10.0.1", + "bfj": "^7.0.2", + "browserslist": "^4.18.1", + "camelcase": "^6.2.1", + "case-sensitive-paths-webpack-plugin": "^2.4.0", + "css-loader": "^6.5.1", + "css-minimizer-webpack-plugin": "^3.2.0", + "dotenv": "^10.0.0", + "dotenv-expand": "^5.1.0", + "eslint": "^8.3.0", + "eslint-config-react-app": "^7.0.1", + "eslint-webpack-plugin": "^3.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^10.0.0", + "html-webpack-plugin": "^5.5.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^27.4.3", + "jest-resolve": "^27.4.2", + "jest-watch-typeahead": "^1.0.0", + "mini-css-extract-plugin": "^2.4.5", + "postcss": "^8.4.4", + "postcss-flexbugs-fixes": "^5.0.2", + "postcss-loader": "^6.2.1", + "postcss-normalize": "^10.0.1", + "postcss-preset-env": "^7.0.1", + "prompts": "^2.4.2", + "react-app-polyfill": "^3.0.0", + "react-dev-utils": "^12.0.1", + "react-refresh": "^0.11.0", + "resolve": "^1.20.0", + "resolve-url-loader": "^4.0.0", + "sass-loader": "^12.3.0", + "semver": "^7.3.5", + "source-map-loader": "^3.0.0", + "style-loader": "^3.3.1", + "tailwindcss": "^3.0.2", + "terser-webpack-plugin": "^5.2.5", + "webpack": "^5.64.4", + "webpack-dev-server": "^4.6.0", + "webpack-manifest-plugin": "^4.0.2", + "workbox-webpack-plugin": "^6.4.1" + }, + "bin": { + "react-scripts": "bin/react-scripts.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + }, + "peerDependencies": { + "react": ">= 16", + "typescript": "^3.2.1 || ^4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/react-scripts/node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "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", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "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==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/regex-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", + "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^7.0.35", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=8.9" + }, + "peerDependencies": { + "rework": "1.0.1", + "rework-visit": "1.0.0" + }, + "peerDependenciesMeta": { + "rework": { + "optional": true + }, + "rework-visit": { + "optional": true + } + } + }, + "node_modules/resolve-url-loader/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/resolve-url-loader/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "license": "ISC" + }, + "node_modules/resolve-url-loader/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "license": "MIT", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve.exports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", + "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "license": "MIT", + "dependencies": { + "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": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + ], + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "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==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sanitize.css": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", + "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==", + "license": "CC0-1.0" + }, + "node_modules/sass-loader": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", + "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "license": "MIT", + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "license": "ISC" + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "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/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "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/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==", + "license": "MIT", + "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/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "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==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", + "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.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==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "license": "MIT" + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", + "license": "MIT" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "license": "MIT", + "dependencies": { + "escodegen": "^1.8.1" + } + }, + "node_modules/static-eval/node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/static-eval/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/static-eval/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/static-eval/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/static-eval/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/static-eval/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-eval/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdin": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/stdin/-/stdin-0.0.1.tgz", + "integrity": "sha512-2bacd1TXzqOEsqRa+eEWkRdOSznwptrs4gqFcpMq5tOtmJUGPZd10W5Lam6wQ4YQ/+qjQt4e9u35yXCF6mrlfQ==" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "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==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==", + "license": "MIT" + }, + "node_modules/string-width/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==", + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "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==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "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.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "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==", + "license": "MIT", + "dependencies": { + "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.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", + "dependencies": { + "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" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/style-to-js": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", + "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.9" + } + }, + "node_modules/style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, + "node_modules/svgdom": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/svgdom/-/svgdom-0.1.23.tgz", + "integrity": "sha512-zTT8cz8rf07OCvRhTipJv+bt/MgL5Zm2ZF3IttK9zr/MKVDTrgo+usbDQMOK1PGzqDT7GA1SshnYlXvtafK7Fw==", + "license": "MIT", + "dependencies": { + "fontkit": "^2.0.4", + "image-size": "^1.2.1", + "sax": "^1.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + } + }, + "node_modules/svgdom/node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/svgo/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/svgo/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==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/svgo/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==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/svgo/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==", + "license": "MIT" + }, + "node_modules/svgo/node_modules/css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "node_modules/svgo/node_modules/css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "license": "BSD-2-Clause" + }, + "node_modules/svgo/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==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/svgo/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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/svgo/node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/svgo/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==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.40.0.tgz", + "integrity": "sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "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.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "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/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==", + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "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==", + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throat": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", + "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", + "license": "MIT" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "license": "MIT" + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "license": "MIT" + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "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/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "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==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "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==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "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==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "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==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^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" + } + }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", + "license": "MIT" + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", + "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", + "license": "ISC", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", + "license": "MIT", + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/web-vitals": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", + "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10.4" + } + }, + "node_modules/webpack": { + "version": "5.99.9", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", + "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "@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": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "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-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.4", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-manifest-plugin": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", + "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==", + "license": "MIT", + "dependencies": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "webpack": "^4.44.2 || ^5.47.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "license": "MIT", + "dependencies": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.2.tgz", + "integrity": "sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/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==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/whatwg-encoding/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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "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.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==", + "license": "MIT", + "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.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workbox-background-sync": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", + "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==", + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz", + "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==", + "license": "MIT", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-build": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", + "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==", + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-replace": "^2.4.1", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "rollup-plugin-terser": "^7.0.0", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "6.6.0", + "workbox-broadcast-update": "6.6.0", + "workbox-cacheable-response": "6.6.0", + "workbox-core": "6.6.0", + "workbox-expiration": "6.6.0", + "workbox-google-analytics": "6.6.0", + "workbox-navigation-preload": "6.6.0", + "workbox-precaching": "6.6.0", + "workbox-range-requests": "6.6.0", + "workbox-recipes": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0", + "workbox-streams": "6.6.0", + "workbox-sw": "6.6.0", + "workbox-window": "6.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "license": "MIT", + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/workbox-build/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/workbox-build/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/workbox-build/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workbox-build/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/workbox-build/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "license": "BSD-2-Clause" + }, + "node_modules/workbox-build/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz", + "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", + "deprecated": "workbox-background-sync@6.6.0", + "license": "MIT", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-core": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz", + "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==", + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz", + "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==", + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-google-analytics": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz", + "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", + "deprecated": "It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained", + "license": "MIT", + "dependencies": { + "workbox-background-sync": "6.6.0", + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz", + "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==", + "license": "MIT", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-precaching": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz", + "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==", + "license": "MIT", + "dependencies": { + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" + } + }, + "node_modules/workbox-range-requests": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz", + "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==", + "license": "MIT", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-recipes": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", + "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==", + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "6.6.0", + "workbox-core": "6.6.0", + "workbox-expiration": "6.6.0", + "workbox-precaching": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" + } + }, + "node_modules/workbox-routing": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz", + "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==", + "license": "MIT", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-strategies": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz", + "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==", + "license": "MIT", + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-streams": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz", + "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==", + "license": "MIT", + "dependencies": { + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0" + } + }, + "node_modules/workbox-sw": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", + "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==", + "license": "MIT" + }, + "node_modules/workbox-webpack-plugin": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.0.tgz", + "integrity": "sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A==", + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "^2.1.0", + "pretty-bytes": "^5.4.1", + "upath": "^1.2.0", + "webpack-sources": "^1.4.3", + "workbox-build": "6.6.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "webpack": "^4.4.0 || ^5.9.0" + } + }, + "node_modules/workbox-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "license": "MIT", + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/workbox-window": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz", + "integrity": "sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "6.6.0" + } + }, + "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==", + "license": "MIT", + "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/wrap-ansi-cjs": { + "name": "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==", + "license": "MIT", + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "license": "Apache-2.0" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/phd-advisor-frontend/package.json b/phd-advisor-frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..a3fc60cad8dbe11b3067c1902231d634d17d78de --- /dev/null +++ b/phd-advisor-frontend/package.json @@ -0,0 +1,48 @@ +{ + "name": "phd-advisor-frontend", + "version": "0.1.0", + "private": true, + "dependencies": { + "@codemirror/legacy-modes": "^6.5.2", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^13.5.0", + "@uiw/react-codemirror": "^4.25.9", + "katex": "^0.16.45", + "latex.js": "^0.12.6", + "lucide-react": "^0.544.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-markdown": "^10.1.0", + "react-scripts": "5.0.1", + "rehype-katex": "^7.0.1", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/phd-advisor-frontend/public/favicon.ico b/phd-advisor-frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a Binary files /dev/null and b/phd-advisor-frontend/public/favicon.ico differ diff --git a/phd-advisor-frontend/public/index.html b/phd-advisor-frontend/public/index.html new file mode 100644 index 0000000000000000000000000000000000000000..01f6dac0c097216fb8b884de09cf35bc3501441d --- /dev/null +++ b/phd-advisor-frontend/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + LaunchPadAI + + + +
+ + + diff --git a/phd-advisor-frontend/public/logo192.png b/phd-advisor-frontend/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..4652f36958ba722e822d2da52eb0e64ce4388341 --- /dev/null +++ b/phd-advisor-frontend/public/logo192.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c386396ec70db3608075b5fbfaac4ab1ccaa86ba05a68ab393ec551eb66c3e00 +size 5347 diff --git a/phd-advisor-frontend/public/logo512.png b/phd-advisor-frontend/public/logo512.png new file mode 100644 index 0000000000000000000000000000000000000000..ba6e512c930a5d395e72ef6a5daef2e83ac93586 --- /dev/null +++ b/phd-advisor-frontend/public/logo512.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ea4f4da7050c0cc408926f6a39c253624e9babb1d43c7977cd821445a60b461 +size 9664 diff --git a/phd-advisor-frontend/public/manifest.json b/phd-advisor-frontend/public/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..6f13bef8861a5de182ef4ffd72d47f5c50107634 --- /dev/null +++ b/phd-advisor-frontend/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "LaunchPadAI", + "name": "LaunchPadAI โ€” AI mentor for internships to full-time jobs", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#0F766E", + "background_color": "#ffffff" +} diff --git a/phd-advisor-frontend/public/neon-logo.png b/phd-advisor-frontend/public/neon-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..648381c66bab703ed5e2eaf52d67284e0d8e78fb --- /dev/null +++ b/phd-advisor-frontend/public/neon-logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c9839f8c25e457a57c2d30bffefbf7d31607b071ad85004445054f899c5ec28 +size 130156 diff --git a/phd-advisor-frontend/public/robots.txt b/phd-advisor-frontend/public/robots.txt new file mode 100644 index 0000000000000000000000000000000000000000..e9e57dc4d41b9b46e05112e9f45b7ea6ac0ba15e --- /dev/null +++ b/phd-advisor-frontend/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/phd-advisor-frontend/src/App.js b/phd-advisor-frontend/src/App.js new file mode 100644 index 0000000000000000000000000000000000000000..e0624a26a8ee671d1b9592bfbd9792aa65ce3e04 --- /dev/null +++ b/phd-advisor-frontend/src/App.js @@ -0,0 +1,167 @@ +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { ThemeProvider } from './contexts/ThemeContext'; +import { AppConfigProvider } from './contexts/AppConfigContext'; +import HomePage from './pages/HomePage'; +import ChatPage from './pages/ChatPage'; +import AuthPage from './pages/AuthPage'; +import CanvasPage from './pages/CanvasPage'; +import UserGuide from './components/UserGuide'; +import { + readStoredAuth, + persistAuth, + clearStoredAuth, + getApiBaseUrl, +} from './utils/authStorage'; +import './styles/components.css'; + +function App() { + const initialAuthRef = useRef(null); + if (initialAuthRef.current === null) { + initialAuthRef.current = readStoredAuth(); + } + const initialAuth = initialAuthRef.current; + const [currentView, setCurrentView] = useState(initialAuth ? 'chat' : 'home'); + const [isAuthenticated, setIsAuthenticated] = useState(Boolean(initialAuth)); + const [user, setUser] = useState(initialAuth?.user ?? null); + const [authToken, setAuthToken] = useState(initialAuth?.token ?? null); + const [authBootstrapping, setAuthBootstrapping] = useState(Boolean(initialAuth)); + + const clearAuthState = useCallback(() => { + clearStoredAuth(); + setUser(null); + setAuthToken(null); + setIsAuthenticated(false); + setCurrentView('home'); + }, []); + + // Re-validate stored credentials on startup so stale tokens are cleared + // and profile data stays fresh after refresh. + useEffect(() => { + if (!initialAuth) { + return; + } + + let cancelled = false; + + const validateStoredSession = async () => { + try { + const response = await fetch(`${getApiBaseUrl()}/auth/me`, { + headers: { Authorization: `Bearer ${initialAuth.token}` }, + }); + + if (cancelled) { + return; + } + + if (response.ok) { + const freshUser = await response.json(); + persistAuth(freshUser, initialAuth.token); + setUser(freshUser); + setAuthToken(initialAuth.token); + setIsAuthenticated(true); + return; + } + + if (response.status === 401 || response.status === 403) { + clearAuthState(); + } + } catch { + // Keep the cached session on transient network errors so refresh + // does not force re-login when the API is briefly unavailable. + } finally { + if (!cancelled) { + setAuthBootstrapping(false); + } + } + }; + + validateStoredSession(); + + return () => { + cancelled = true; + }; + }, [initialAuth, clearAuthState]); + + const sessionReady = isAuthenticated && !authBootstrapping; + + const navigateToAuth = () => { + if (authBootstrapping) { + return; + } + setCurrentView('auth'); + }; + + const navigateToCanvas = (canvasView) => { + if (['insights', 'workspace', 'deliverables'].includes(canvasView)) { + localStorage.setItem('canvas-view-v2', canvasView); + } + setCurrentView('canvas'); + }; + + const navigateToChat = () => { + setCurrentView('chat'); + }; + + const navigateToHome = () => { + setCurrentView('home'); + }; + + const handleAuthSuccess = (userData, token) => { + persistAuth(userData, token); + setUser(userData); + setAuthToken(token); + setIsAuthenticated(true); + setAuthBootstrapping(false); + setCurrentView('chat'); + }; + + const handleSignOut = () => { + clearAuthState(); + }; + + return ( + + +
+ {authBootstrapping && ( +
+ Restoring your session... +
+ )} + {currentView === 'home' && ( + + )} + {currentView === 'auth' && !sessionReady && ( + + )} + {currentView === 'canvas' && sessionReady && ( + + )} + {currentView === 'chat' && sessionReady && ( + + )} + +
+
+
+ ); +} + +export default App; diff --git a/phd-advisor-frontend/src/App.test.js b/phd-advisor-frontend/src/App.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1f03afeece5ac28064fa3c73a29215037465f789 --- /dev/null +++ b/phd-advisor-frontend/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/phd-advisor-frontend/src/components/AccountModal.js b/phd-advisor-frontend/src/components/AccountModal.js new file mode 100644 index 0000000000000000000000000000000000000000..e71f48dda5d02acbda6abd2bccd5b4bcba033f0c --- /dev/null +++ b/phd-advisor-frontend/src/components/AccountModal.js @@ -0,0 +1,339 @@ +import React, { useState } from 'react'; +import { User, Mail, Save, Trash2, AlertTriangle, X, Loader2, CheckCircle, Lock, ChevronDown, ChevronUp, Eye, EyeOff } from 'lucide-react'; +import { useTheme } from '../contexts/ThemeContext'; + +const AccountModal = ({ user, authToken, onClose, onAccountUpdated, onAccountDeleted }) => { + const { isDark } = useTheme(); + const [firstName, setFirstName] = useState(user?.firstName || ''); + const [lastName, setLastName] = useState(user?.lastName || ''); + const [email, setEmail] = useState(user?.email || ''); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + const [error, setError] = useState(null); + const [showPassword, setShowPassword] = useState(false); + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [changingPw, setChangingPw] = useState(false); + const [pwSuccess, setPwSuccess] = useState(false); + const [pwError, setPwError] = useState(null); + const [showCurrent, setShowCurrent] = useState(false); + const [showNew, setShowNew] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleting, setDeleting] = useState(false); + const [deleteInput, setDeleteInput] = useState(''); + + const hasChanges = + firstName !== (user?.firstName || '') || + lastName !== (user?.lastName || '') || + email !== (user?.email || ''); + + const handleSave = async () => { + if (!firstName.trim() || !email.trim()) { + setError('First name and email are required.'); + return; + } + setSaving(true); + setError(null); + try { + const resp = await fetch(`${process.env.REACT_APP_API_URL}/auth/me`, { + method: 'PATCH', + headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ firstName: firstName.trim(), lastName: lastName.trim(), email: email.trim() }), + }); + if (resp.ok) { + const updated = await resp.json(); + setSaved(true); + if (onAccountUpdated) onAccountUpdated(updated); + setTimeout(() => setSaved(false), 2000); + } else { + const data = await resp.json().catch(() => ({})); + setError(data.detail || 'Failed to update account.'); + } + } catch { + setError('Network error.'); + } finally { + setSaving(false); + } + }; + + const handleChangePassword = async () => { + setPwError(null); + if (!currentPassword) { setPwError('Enter your current password.'); return; } + if (newPassword.length < 6) { setPwError('New password must be at least 6 characters.'); return; } + if (newPassword !== confirmPassword) { setPwError('New passwords do not match.'); return; } + setChangingPw(true); + try { + const resp = await fetch(`${process.env.REACT_APP_API_URL}/auth/me/password`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }), + }); + if (resp.ok) { + setPwSuccess(true); + setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); + setTimeout(() => { setPwSuccess(false); setShowPassword(false); }, 2000); + } else { + const data = await resp.json().catch(() => ({})); + setPwError(data.detail || 'Failed to change password.'); + } + } catch { + setPwError('Network error.'); + } finally { + setChangingPw(false); + } + }; + + const handleDelete = async () => { + setDeleting(true); + try { + const resp = await fetch(`${process.env.REACT_APP_API_URL}/auth/me`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${authToken}` }, + }); + if (resp.ok) { + if (onAccountDeleted) onAccountDeleted(); + } else { + setError('Failed to delete account.'); + setShowDeleteConfirm(false); + } + } catch { + setError('Network error.'); + setShowDeleteConfirm(false); + } finally { + setDeleting(false); + } + }; + + const overlay = { + position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', + display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999, + }; + const modal = { + background: isDark ? '#1f2937' : '#fff', + borderRadius: 16, padding: 28, width: 420, maxWidth: '92vw', + boxShadow: '0 20px 60px rgba(0,0,0,0.3)', + color: isDark ? '#f3f4f6' : '#111827', + }; + const inputStyle = { + width: '100%', padding: '10px 12px', borderRadius: 10, fontSize: 14, + border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`, + background: isDark ? '#111827' : '#f9fafb', + color: isDark ? '#f3f4f6' : '#111827', outline: 'none', + boxSizing: 'border-box', + }; + const labelStyle = { + display: 'block', fontSize: 12, fontWeight: 600, + color: isDark ? '#9ca3af' : '#6b7280', marginBottom: 5, + }; + + if (showDeleteConfirm) { + return ( +
setShowDeleteConfirm(false)}> +
e.stopPropagation()}> +
+ +

Delete Account?

+

+ This will permanently delete your account and all associated data + (profile, chats, canvas). This action cannot be undone. +

+

+ Type DELETE to confirm: +

+ setDeleteInput(e.target.value)} + placeholder="DELETE" + style={{ ...inputStyle, textAlign: 'center', maxWidth: 200, margin: '0 auto 18px' }} + /> +
+ + +
+
+
+
+ ); + } + + return ( +
+
e.stopPropagation()}> + {/* Header */} +
+
+ +

Account

+
+ +
+ + {/* Fields */} +
+
+ + setFirstName(e.target.value)} style={inputStyle} /> +
+
+ + setLastName(e.target.value)} style={inputStyle} /> +
+
+ +
+ +
+ + setEmail(e.target.value)} type="email" style={{ ...inputStyle, paddingLeft: 36 }} /> +
+
+ + {/* Change Password */} +
+ + {showPassword && ( +
+
+ +
+ setCurrentPassword(e.target.value)} + type={showCurrent ? 'text' : 'password'} style={inputStyle} placeholder="Enter current password" /> + +
+
+
+ +
+ setNewPassword(e.target.value)} + type={showNew ? 'text' : 'password'} style={inputStyle} placeholder="At least 6 characters" /> + +
+
+
+ + setConfirmPassword(e.target.value)} + type={showNew ? 'text' : 'password'} style={inputStyle} placeholder="Re-enter new password" /> +
+ {pwError && ( +
{pwError}
+ )} + +
+ )} +
+ + {error && ( +
+ {error} +
+ )} + + {/* Save */} +
+ + + +
+
+
+ ); +}; + +export default AccountModal; diff --git a/phd-advisor-frontend/src/components/AdvisorCard.js b/phd-advisor-frontend/src/components/AdvisorCard.js new file mode 100644 index 0000000000000000000000000000000000000000..cf98a947b29864371d2c863373ba5433d8a1839e --- /dev/null +++ b/phd-advisor-frontend/src/components/AdvisorCard.js @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { Pencil } from 'lucide-react'; +import { useAppConfig } from '../contexts/AppConfigContext'; +import { useTheme } from '../contexts/ThemeContext'; +import AvatarPickerModal from './AvatarPickerModal'; + +const AdvisorCard = ({ advisor, advisorId }) => { + const Icon = advisor.icon; + const { isDark } = useTheme(); + const { getAdvisorColors } = useAppConfig(); + const colors = getAdvisorColors(advisorId, isDark); + const [hovered, setHovered] = useState(false); + const [pickerOpen, setPickerOpen] = useState(false); + + return ( + <> +
+
setHovered(true)} + onMouseLeave={() => setHovered(false)} + onClick={() => setPickerOpen(true)} + > + {advisor.avatarUrl ? ( + {advisor.name} + ) : ( + + )} + {hovered && ( +
+ +
+ )} +
+

{advisor.name}

+

{advisor.role}

+

{advisor.description}

+
+ {pickerOpen && ( + setPickerOpen(false)} /> + )} + + ); +}; + +export default AdvisorCard; diff --git a/phd-advisor-frontend/src/components/AdvisorCarousel.js b/phd-advisor-frontend/src/components/AdvisorCarousel.js new file mode 100644 index 0000000000000000000000000000000000000000..bb4c0826e7a7394ce474a1653e968e2b61505eb1 --- /dev/null +++ b/phd-advisor-frontend/src/components/AdvisorCarousel.js @@ -0,0 +1,119 @@ +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import MessageBubble from './MessageBubble'; + +const CAROUSEL_BREAKPOINT = 700; + +const AdvisorCarousel = ({ messages = [], onReply, onExpand, onClick, onSearchReferences, userAvatarId, userAvatarOptions }) => { + const [activeIndex, setActiveIndex] = useState(0); + const [isCarouselMode, setIsCarouselMode] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + + const observer = new ResizeObserver(entries => { + const width = entries[0].contentRect.width; + setIsCarouselMode(width < CAROUSEL_BREAKPOINT); + }); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + setActiveIndex(0); + }, [messages.length]); + + const goPrev = useCallback(() => { + setActiveIndex(i => Math.max(0, i - 1)); + }, []); + + const goNext = useCallback(() => { + setActiveIndex(i => Math.min(messages.length - 1, i + 1)); + }, [messages.length]); + + if (messages.length === 1) { + return ( +
+ +
+ ); + } + + return ( +
+ {isCarouselMode && ( + + )} + +
+
+ {messages.map(message => ( +
+ +
+ ))} +
+
+ + {isCarouselMode && ( + <> + + +
+ {messages.map((m, i) => ( +
+ + )} +
+ ); +}; + +export default AdvisorCarousel; diff --git a/phd-advisor-frontend/src/components/AdvisorStatusDropdown.js b/phd-advisor-frontend/src/components/AdvisorStatusDropdown.js new file mode 100644 index 0000000000000000000000000000000000000000..eaec978075187300c9418c2af5fb2ad3b2f973aa --- /dev/null +++ b/phd-advisor-frontend/src/components/AdvisorStatusDropdown.js @@ -0,0 +1,474 @@ +import React, { useState, useEffect } from 'react'; +import { Users, ChevronDown, Pencil } from 'lucide-react'; +import AvatarPickerModal from './AvatarPickerModal'; + +const AdvisorStatusDropdown = ({ + advisors, + activeAdvisorIds = [], + onToggleAdvisor, + onSetActiveAdvisors, + thinkingAdvisors, + getAdvisorColors, + isDark, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [hoveredId, setHoveredId] = useState(null); + const [pickerAdvisor, setPickerAdvisor] = useState(null); + + useEffect(() => { + const handleClickOutside = (event) => { + if (isOpen && !event.target.closest('.advisor-status-dropdown')) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [isOpen]); + + if (!advisors || typeof advisors !== 'object') { + return null; + } + + const advisorEntries = Object.entries(advisors); + const allIds = advisorEntries.map(([id]) => id); + const activeSet = new Set(activeAdvisorIds); + const activeCount = allIds.filter((id) => activeSet.has(id)).length; + const thinkingCount = Array.isArray(thinkingAdvisors) + ? thinkingAdvisors.filter((id) => id !== 'system' && activeSet.has(id)).length + : 0; + const totalAdvisors = advisorEntries.length; + + const handleToggle = () => { + setIsOpen(!isOpen); + }; + + const handleCheckboxChange = (id, event) => { + event.stopPropagation(); + if (onToggleAdvisor) { + onToggleAdvisor(id); + } + }; + + const selectAll = (event) => { + event.stopPropagation(); + if (onSetActiveAdvisors) { + onSetActiveAdvisors([...allIds]); + } + }; + + const selectNone = (event) => { + event.stopPropagation(); + if (onSetActiveAdvisors && allIds.length > 0) { + onSetActiveAdvisors([allIds[0]]); + } + }; + + return ( +
+ + + {pickerAdvisor && ( + setPickerAdvisor(null)} + /> + )} + {isOpen && ( +
+
+
Active advisors
+
+ Only checked advisors respond to your messages. +
+
+ + ยท + +
+
+
+ {advisorEntries.map(([id, advisor]) => { + const IconComponent = advisor.icon; + const colors = getAdvisorColors(id, isDark); + const isThinking = Array.isArray(thinkingAdvisors) && thinkingAdvisors.includes(id); + const isActive = activeSet.has(id); + + return ( +
+ +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setPickerAdvisor({ id, name: advisor.name }); + } + }} + onClick={() => setPickerAdvisor({ id, name: advisor.name })} + onMouseEnter={() => setHoveredId(id)} + onMouseLeave={() => setHoveredId(null)} + > + {advisor.avatarUrl + ? {advisor.name} + : } + {hoveredId === id && ( +
+ +
+ )} +
+
+
{advisor.name}
+
{advisor.description}
+
+
+ {!isActive ? ( +
Off
+ ) : isThinking ? ( +
+
+
+
+
+
+
+ ) : ( +
Ready
+ )} +
+
+ ); + })} +
+
+ )} + + +
+ ); +}; + +export default AdvisorStatusDropdown; diff --git a/phd-advisor-frontend/src/components/AppHeader.js b/phd-advisor-frontend/src/components/AppHeader.js new file mode 100644 index 0000000000000000000000000000000000000000..02810ebb7b7c4765b184988c00cd4fcc386dc44f --- /dev/null +++ b/phd-advisor-frontend/src/components/AppHeader.js @@ -0,0 +1,105 @@ +import React from 'react'; +import { Home, Menu, Users } from 'lucide-react'; +import ThemeToggle from './ThemeToggle'; +import { useAppConfig } from '../contexts/AppConfigContext'; + +/** + * Shared floating header used on every page so the app feels like one surface. + * + * Props: + * currentPage: 'home' | 'chat' | 'canvas' + * onNavigateToHome, onNavigateToChat, onNavigateToCanvas: navigation callbacks + * (onNavigateToCanvas may receive 'insights' | 'workspace' to deep-link a view) + * onMobileMenu?: () => void โ€” when present, shows the mobile menu button + * children?: ReactNode โ€” extra controls slotted between the tabs and the theme toggle + */ +const AppHeader = ({ + currentPage = 'home', + onNavigateToHome, + onNavigateToChat, + onNavigateToCanvas, + onMobileMenu, + children, +}) => { + const { config, resolveIcon } = useAppConfig(); + const logoIconName = config?.app?.logo_icon || 'Users'; + const BrandIcon = resolveIcon ? resolveIcon(logoIconName) : Users; + + const goToCanvas = (view) => { + if (onNavigateToCanvas) onNavigateToCanvas(view); + }; + + // Accept either 'canvas' (all canvas tabs highlight equally) or a more specific + // 'canvas-' from CanvasPage so only the active one highlights. + const isOnHome = currentPage === 'home'; + const isOnChat = currentPage === 'chat'; + const isOnCanvas = currentPage === 'canvas' || currentPage.startsWith('canvas-'); + const canvasSub = currentPage.startsWith('canvas-') ? currentPage.slice(7) : null; + const tabActive = (sub) => isOnCanvas && (canvasSub === null ? false : canvasSub === sub); + + return ( +
+
+ {onMobileMenu && ( + + )} + +
+
+ +
+
+

{config?.app?.title || 'Advisory'}

+

{config?.app?.subtitle || 'AI-Powered Guidance'}

+
+
+
+ + {/* Hide the view pill bar on the home page โ€” home is a landing page, + not part of the chat โ†” canvas surface. */} + {!isOnHome && ( +
+ + + + +
+ )} + + {/* Compact mobile dropdown โ€” appears in place of the pill bar at narrow widths */} + {!isOnHome && ( + + )} + +
+ {children} + +
+
+ ); +}; + +export default AppHeader; diff --git a/phd-advisor-frontend/src/components/AvatarPickerModal.js b/phd-advisor-frontend/src/components/AvatarPickerModal.js new file mode 100644 index 0000000000000000000000000000000000000000..a7ac07a4dd867a81e0a9130c3c03d3017aee037a --- /dev/null +++ b/phd-advisor-frontend/src/components/AvatarPickerModal.js @@ -0,0 +1,75 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { X } from 'lucide-react'; +import { useAppConfig } from '../contexts/AppConfigContext'; + +const API = process.env.REACT_APP_API_URL || ''; + +const BUNDLED = [ + 'advisor1.png','advisor2.png','advisor3.png','advisor4.png', + 'advisor5.png','advisor6.png','advisor7.png', +]; + +const overlay = { + position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', + display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, +}; + +const modal = { + background: 'var(--bg-primary)', borderRadius: 16, padding: 24, width: 480, + maxWidth: '95vw', maxHeight: '85vh', overflowY: 'auto', + boxShadow: 'var(--shadow-xl)', +}; + +const AvatarPickerModal = ({ advisorId, advisorName, onClose }) => { + const { setAdvisorAvatar } = useAppConfig(); + + const select = (url) => { + setAdvisorAvatar(advisorId, url || ''); + onClose(); + }; + + return ReactDOM.createPortal( +
e.target === e.currentTarget && onClose()} onMouseDown={(e) => e.stopPropagation()}> +
+
+

+ Choose Avatar โ€” {advisorName} +

+ +
+ +

Pre-made Avatars

+
+ {BUNDLED.map((file) => ( + {file} select(`${API}/api/avatars/bundled/${file}`)} + style={{ width: '100%', aspectRatio: '1', borderRadius: '50%', objectFit: 'cover', cursor: 'pointer', border: '2px solid transparent', transition: 'border-color 0.15s' }} + onMouseEnter={e => e.target.style.borderColor = 'var(--accent-primary)'} + onMouseLeave={e => e.target.style.borderColor = 'transparent'} + /> + ))} +
+ + +
+
, + document.body + ); +}; + +export default AvatarPickerModal; diff --git a/phd-advisor-frontend/src/components/ChatInput.js b/phd-advisor-frontend/src/components/ChatInput.js new file mode 100644 index 0000000000000000000000000000000000000000..a36e9be96d62fa14bf9c9f7b9651eb5cf80e0772 --- /dev/null +++ b/phd-advisor-frontend/src/components/ChatInput.js @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { Send } from 'lucide-react'; + +const ChatInput = ({ onSendMessage, isLoading, placeholder = "Ask your advisors anything..." }) => { + const [inputMessage, setInputMessage] = useState(''); + + const handleSend = () => { + if (!inputMessage.trim() || isLoading) return; + + onSendMessage(inputMessage); + setInputMessage(''); + }; + + const handleKeyPress = (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + return ( +
+
+