diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..6b1277808825bcb23c146bfbee34e8f7cfff9202 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +Dockerfile +.vscode/ +.idea +.gitignore +LICENSE +README.md +node_modules/ +.svelte-kit/ +.env* +!.env +.env.local +db \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000000000000000000000000000000000000..a58ed42819f3d32a8a5dfdecd2c0bd9dd834afcb --- /dev/null +++ b/.env @@ -0,0 +1,197 @@ +# Use .env.local to change these variables +# DO NOT EDIT THIS FILE WITH SENSITIVE DATA + +### MongoDB ### +MONGODB_URL=#your mongodb URL here, use chat-ui-db image if you don't want to set this +MONGODB_DB_NAME=chat-ui +MONGODB_DIRECT_CONNECTION=false + + +### Endpoints config ### +HF_API_ROOT=https://api-inference.huggingface.co/models +# HF_TOKEN is used for a lot of things, not only for inference but also fetching tokenizers, etc. +# We recommend using an HF_TOKEN even if you use a local endpoint. +HF_TOKEN= #get it from https://huggingface.co/settings/token +# API Keys for providers, you will need to specify models in the MODELS section but these keys can be kept secret +OPENAI_API_KEY=#your openai api key here +ANTHROPIC_API_KEY=#your anthropic api key here +CLOUDFLARE_ACCOUNT_ID=#your cloudflare account id here +CLOUDFLARE_API_TOKEN=#your cloudflare api token here +COHERE_API_TOKEN=#your cohere api token here +GOOGLE_GENAI_API_KEY=#your google genai api token here + + +### Models ### +## Models can support many different endpoints, check the documentation for more details +MODELS=`[ + { + "name": "NousResearch/Hermes-3-Llama-3.1-8B", + "description": "Nous Research's latest Hermes 3 release in 8B size.", + "promptExamples": [ + { + "title": "Write an email from bullet list", + "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)" + }, { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + }, { + "title": "Assist in a task", + "prompt": "How do I make a delicious lemon cheesecake?" + } + ] + } +]` +## Text Embedding Models used for websearch +# Default is a model that runs locally on CPU. +TEXT_EMBEDDING_MODELS = `[ + { + "name": "Xenova/gte-small", + "displayName": "Xenova/gte-small", + "description": "Local embedding model running on the server.", + "chunkCharLength": 512, + "endpoints": [ + { "type": "transformersjs" } + ] + } +]` + +## Removed models, useful for migrating conversations +# { name: string, displayName?: string, id?: string, transferTo?: string }` +OLD_MODELS=`[]` + +## Task model +# name of the model used for tasks such as summarizing title, creating query, etc. +# if not set, the first model in MODELS will be used +TASK_MODEL= + + +### Authentication ### +# Parameters to enable open id login +OPENID_CONFIG= +MESSAGES_BEFORE_LOGIN=# how many messages a user can send in a conversation before having to login. set to 0 to force login right away +# if it's defined, only these emails will be allowed to use login +ALLOWED_USER_EMAILS=`[]` +# If it's defined, users with emails matching these domains will also be allowed to use login +ALLOWED_USER_DOMAINS=`[]` +# valid alternative redirect URLs for OAuth, used for HuggingChat apps +ALTERNATIVE_REDIRECT_URLS=`[]` +### Cookies +# name of the cookie used to store the session +COOKIE_NAME=hf-chat +# specify secure behaviour for cookies +COOKIE_SAMESITE=# can be "lax", "strict", "none" or left empty +COOKIE_SECURE=# set to true to only allow cookies over https + + +### Websearch ### +## API Keys used to activate search with web functionality. websearch is disabled if none are defined. choose one of the following: +YDC_API_KEY=#your docs.you.com api key here +SERPER_API_KEY=#your serper.dev api key here +SERPAPI_KEY=#your serpapi key here +SERPSTACK_API_KEY=#your serpstack api key here +SEARCHAPI_KEY=#your searchapi api key here +USE_LOCAL_WEBSEARCH=#set to true to parse google results yourself, overrides other API keys +SEARXNG_QUERY_URL=# where '' will be replaced with query keywords see https://docs.searxng.org/dev/search_api.html eg https://searxng.yourdomain.com/search?q=&engines=duckduckgo,google&format=json +BING_SUBSCRIPTION_KEY=#your key +## Websearch configuration +PLAYWRIGHT_ADBLOCKER=true +WEBSEARCH_ALLOWLIST=`[]` # if it's defined, allow websites from only this list. +WEBSEARCH_BLOCKLIST=`[]` # if it's defined, block websites from this list. +WEBSEARCH_JAVASCRIPT=true # CPU usage reduces by 60% on average by disabling javascript. Enable to improve website compatibility +WEBSEARCH_TIMEOUT = 3500 # in milliseconds, determines how long to wait to load a page before timing out +ENABLE_LOCAL_FETCH=false #set to true to allow fetches on the local network. /!\ Only enable this if you have the proper firewall rules to prevent SSRF attacks and understand the implications. + + +## Public app configuration ## +PUBLIC_APP_GUEST_MESSAGE=# a message to the guest user. If not set, no message will be shown. Only used if you have authentication enabled. +PUBLIC_APP_NAME=Mit # name used as title throughout the app +PUBLIC_APP_ASSETS=chatui # used to find logos & favicons in static/$PUBLIC_APP_ASSETS +PUBLIC_APP_DESCRIPTION=# description used throughout the app +PUBLIC_APP_DATA_SHARING=# Set to 1 to enable an option in the user settings to share conversations with model authors +PUBLIC_APP_DISCLAIMER=# Set to 1 to show a disclaimer on login page +PUBLIC_APP_DISCLAIMER_MESSAGE=# Message to show on the login page +PUBLIC_ANNOUNCEMENT_BANNERS=`[ + +]` +PUBLIC_SMOOTH_UPDATES=false # set to true to enable smoothing of messages client-side, can be CPU intensive +PUBLIC_ORIGIN=#https://huggingface.co +PUBLIC_SHARE_PREFIX=#https://hf.co/chat + +# mostly huggingchat specific +PUBLIC_GOOGLE_ANALYTICS_ID=#G-XXXXXXXX / Leave empty to disable +PUBLIC_PLAUSIBLE_SCRIPT_URL=#/js/script.js / Leave empty to disable +PUBLIC_APPLE_APP_ID=#1234567890 / Leave empty to disable + + +### Feature Flags ### +LLM_SUMMARIZATION=true # generate conversation titles with LLMs +ENABLE_ASSISTANTS=false #set to true to enable assistants feature +ENABLE_ASSISTANTS_RAG=false # /!\ This will let users specify arbitrary URLs that the server will then request. Make sure you have the proper firewall rules in place. +REQUIRE_FEATURED_ASSISTANTS=false # require featured assistants to show in the list +COMMUNITY_TOOLS=false # set to true to enable community tools +ALLOW_IFRAME=true # Allow the app to be embedded in an iframe + + +### Tools ### +# Check out public config in `chart/env/prod.yaml` for more details +TOOLS=`[]` + +### Rate limits ### +# See `src/lib/server/usageLimits.ts` +# { +# conversations: number, # how many conversations +# messages: number, # how many messages in a conversation +# assistants: number, # how many assistants +# messageLength: number, # how long can a message be before we cut it off +# messagesPerMinute: number, # how many messages per minute +# tools: number # how many tools +# } +USAGE_LIMITS=`{}` + + +### HuggingFace specific ### +# Let user authenticate with their HF token in the /api routes. This is only useful if you have OAuth configured with huggingface. +USE_HF_TOKEN_IN_API=false +## Feature flag & admin settings +# Used for setting early access & admin flags to users +HF_ORG_ADMIN= +HF_ORG_EARLY_ACCESS= +WEBHOOK_URL_REPORT_ASSISTANT=#provide slack webhook url to get notified for reports/feature requests + + + +### Metrics ### +METRICS_ENABLED=false +METRICS_PORT=5565 +LOG_LEVEL=info + + +### Parquet export ### +# Not in use anymore but useful to export conversations to a parquet file as a HuggingFace dataset +PARQUET_EXPORT_DATASET= +PARQUET_EXPORT_HF_TOKEN= +ADMIN_API_SECRET=# secret to admin API calls, like computing usage stats or exporting parquet data + + +### Docker build variables ### +# These values cannot be updated at runtime +# They need to be passed when building the docker image +# See https://github.com/huggingface/chat-ui/main/.github/workflows/deploy-prod.yml#L44-L47 +APP_BASE="" # base path of the app, e.g. /chat, left blank as default +PUBLIC_APP_COLOR=blue # can be any of tailwind colors: https://tailwindcss.com/docs/customizing-colors#default-color-palette +### Body size limit for SvelteKit https://svelte.dev/docs/kit/adapter-node#Environment-variables-BODY_SIZE_LIMIT +BODY_SIZE_LIMIT=15728640 +PUBLIC_COMMIT_SHA= + +### LEGACY parameters +HF_ACCESS_TOKEN=#LEGACY! Use HF_TOKEN instead +ALLOW_INSECURE_COOKIES=false # LEGACY! Use COOKIE_SECURE and COOKIE_SAMESITE instead +PARQUET_EXPORT_SECRET=#DEPRECATED, use ADMIN_API_SECRET instead +RATE_LIMIT= # /!\ DEPRECATED definition of messages per minute. Use USAGE_LIMITS.messagesPerMinute instead +OPENID_CLIENT_ID= +OPENID_CLIENT_SECRET= +OPENID_SCOPES="openid profile" # Add "email" for some providers like Google that do not provide preferred_username +OPENID_NAME_CLAIM="name" # Change to "username" for some providers that do not provide name +OPENID_PROVIDER_URL=https://huggingface.co # for Google, use https://accounts.google.com +OPENID_TOLERANCE= +OPENID_RESOURCE= diff --git a/.env.ci b/.env.ci new file mode 100644 index 0000000000000000000000000000000000000000..2e0dab4af7f17dc1e632689e30bcc5f45a1f0db7 --- /dev/null +++ b/.env.ci @@ -0,0 +1 @@ +MONGODB_URL=mongodb://localhost:27017/ \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000000000000000000000000000000000..38972655faff07d2cc0383044bbf9f43b22c2248 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000000000000000000000000000000000000..b1aeb0ef85fb179c5b2cdc5c4aa684c4d6700c70 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,44 @@ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:svelte/recommended", + "prettier", + ], + plugins: ["@typescript-eslint"], + ignorePatterns: ["*.cjs"], + overrides: [ + { + files: ["*.svelte"], + parser: "svelte-eslint-parser", + parserOptions: { + parser: "@typescript-eslint/parser", + }, + }, + ], + parserOptions: { + sourceType: "module", + ecmaVersion: 2020, + extraFileExtensions: [".svelte"], + }, + rules: { + "require-yield": "off", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-non-null-assertion": "error", + "@typescript-eslint/no-unused-vars": [ + // prevent variables with a _ prefix from being marked as unused + "error", + { + argsIgnorePattern: "^_", + }, + ], + "object-shorthand": ["error", "always"], + }, + env: { + browser: true, + es2017: true, + node: true, + }, +}; diff --git a/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md b/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md new file mode 100644 index 0000000000000000000000000000000000000000..22a7664a9c01e122c116af38a18fef1ff0c2b7a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md @@ -0,0 +1,43 @@ +--- +name: Bug Report (chat-ui) +about: Use this for confirmed issues with chat-ui +title: "" +labels: bug +assignees: "" +--- + +## Bug description + + + +## Steps to reproduce + + + +## Screenshots + + + +## Context + +### Logs + + + +``` +// logs here if relevant +``` + +### Specs + +- **OS**: +- **Browser**: +- **chat-ui commit**: + +### Config + + + +## Notes + + diff --git a/.github/ISSUE_TEMPLATE/config-support.md b/.github/ISSUE_TEMPLATE/config-support.md new file mode 100644 index 0000000000000000000000000000000000000000..bd858036f15992ec51ca924243b4bbf6363f597e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config-support.md @@ -0,0 +1,9 @@ +--- +name: Config Support +about: Help with setting up chat-ui locally +title: "" +labels: support +assignees: "" +--- + +**Please use the discussions on GitHub** for getting help with setting things up instead of opening an issue: https://github.com/huggingface/chat-ui/discussions diff --git a/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md b/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md new file mode 100644 index 0000000000000000000000000000000000000000..cc9adf91f0f938a12510ecaa104947e198f36196 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md @@ -0,0 +1,17 @@ +--- +name: Feature Request (chat-ui) +about: Suggest new features to be added to chat-ui +title: "" +labels: enhancement +assignees: "" +--- + +## Describe your feature request + + + +## Screenshots (if relevant) + +## Implementation idea + + diff --git a/.github/ISSUE_TEMPLATE/huggingchat.md b/.github/ISSUE_TEMPLATE/huggingchat.md new file mode 100644 index 0000000000000000000000000000000000000000..0716f9baaefb9b69ffaafc1b67f522c5b8753111 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/huggingchat.md @@ -0,0 +1,11 @@ +--- +name: HuggingChat +about: Requests & reporting outages on HuggingChat, the hosted version of chat-ui. +title: "" +labels: huggingchat +assignees: "" +--- + +**Do not use GitHub issues** for requesting models on HuggingChat or reporting issues with HuggingChat being down/overloaded. + +**Use the discussions page on the hub instead:** https://huggingface.co/spaces/huggingchat/chat-ui/discussions diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000000000000000000000000000000000000..3a183679f1aa435bf266e800f343d91ef355eabd --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,16 @@ +changelog: + exclude: + labels: + - huggingchat + - CI/CD + - documentation + categories: + - title: Features + labels: + - enhancement + - title: Bugfixes + labels: + - bug + - title: Other changes + labels: + - "*" diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 0000000000000000000000000000000000000000..cd6109421f3d6f160c174d894f1cc9281cb0903c --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,18 @@ +name: Build documentation + +on: + push: + branches: + - main + - v*-release + +jobs: + build: + uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@main + with: + commit_sha: ${{ github.sha }} + package: chat-ui + additional_args: --not_python_module + secrets: + token: ${{ secrets.HUGGINGFACE_PUSH }} + hf_token: ${{ secrets.HF_DOC_BUILD_PUSH }} diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 0000000000000000000000000000000000000000..846a0f285fe7878d4cd4fcfce3f1a49de80c7a1e --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,140 @@ +name: Build and Publish Image + +permissions: + packages: write + +on: + push: + branches: + - "main" + pull_request: + branches: + - "*" + paths: + - "Dockerfile" + - "entrypoint.sh" + workflow_dispatch: + release: + types: [published, edited] + +jobs: + build-and-publish-image-with-db: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Extract package version + id: package-version + run: | + VERSION=$(jq -r .version package.json) + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + MAJOR=$(echo $VERSION | cut -d '.' -f1) + echo "MAJOR=$MAJOR" >> $GITHUB_OUTPUT + MINOR=$(echo $VERSION | cut -d '.' -f1).$(echo $VERSION | cut -d '.' -f2) + echo "MINOR=$MINOR" >> $GITHUB_OUTPUT + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/huggingface/chat-ui-db + tags: | + type=raw,value=${{ steps.package-version.outputs.VERSION }},enable=${{github.event_name == 'release'}} + type=raw,value=${{ steps.package-version.outputs.MAJOR }},enable=${{github.event_name == 'release'}} + type=raw,value=${{ steps.package-version.outputs.MINOR }},enable=${{github.event_name == 'release'}} + type=raw,value=latest,enable={{is_default_branch}} + type=sha,enable={{is_default_branch}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.5.0 + + - name: Build and Publish Docker Image with DB + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + INCLUDE_DB=true + PUBLIC_COMMIT_SHA=${{ env.GITHUB_SHA_SHORT }} + build-and-publish-image-nodb: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Extract package version + id: package-version + run: | + VERSION=$(jq -r .version package.json) + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + MAJOR=$(echo $VERSION | cut -d '.' -f1) + echo "MAJOR=$MAJOR" >> $GITHUB_OUTPUT + MINOR=$(echo $VERSION | cut -d '.' -f1).$(echo $VERSION | cut -d '.' -f2) + echo "MINOR=$MINOR" >> $GITHUB_OUTPUT + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/huggingface/chat-ui + tags: | + type=raw,value=${{ steps.package-version.outputs.VERSION }},enable=${{github.event_name == 'release'}} + type=raw,value=${{ steps.package-version.outputs.MAJOR }},enable=${{github.event_name == 'release'}} + type=raw,value=${{ steps.package-version.outputs.MINOR }},enable=${{github.event_name == 'release'}} + type=raw,value=latest,enable={{is_default_branch}} + type=sha,enable={{is_default_branch}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.5.0 + + - name: Build and Publish Docker Image without DB + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + INCLUDE_DB=false + PUBLIC_COMMIT_SHA=${{ env.GITHUB_SHA_SHORT }} diff --git a/.github/workflows/build-pr-docs.yml b/.github/workflows/build-pr-docs.yml new file mode 100644 index 0000000000000000000000000000000000000000..9216112730ec29d76465667c80816e131d2c6755 --- /dev/null +++ b/.github/workflows/build-pr-docs.yml @@ -0,0 +1,20 @@ +name: Build PR Documentation + +on: + pull_request: + paths: + - "docs/source/**" + - ".github/workflows/build-pr-docs.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + build: + uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@main + with: + commit_sha: ${{ github.event.pull_request.head.sha }} + pr_number: ${{ github.event.number }} + package: chat-ui + additional_args: --not_python_module diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 0000000000000000000000000000000000000000..859072512933332a4870e2777d2e971f5f169d7f --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,79 @@ +name: Deploy to k8s +on: + # run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build-and-publish-huggingchat-image: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + huggingface/chat-ui + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,enable={{is_default_branch}} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.5.0 + + - name: Build and Publish HuggingChat image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + cache-to: type=gha,mode=max,scope=amd64 + cache-from: type=gha,scope=amd64 + provenance: false + build-args: | + INCLUDE_DB=false + APP_BASE=/chat + PUBLIC_APP_COLOR=yellow + PUBLIC_COMMIT_SHA=${{ env.GITHUB_SHA_SHORT }} + deploy: + name: Deploy on prod + runs-on: ubuntu-latest + needs: ["build-and-publish-huggingchat-image"] + steps: + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.5.0 + + - name: Gen values + run: | + VALUES=$(cat <<-END + image: + tag: "sha-${{ env.GITHUB_SHA_SHORT }}" + END + ) + echo "VALUES=$(echo "$VALUES" | yq -o=json | jq tostring)" >> $GITHUB_ENV + + - name: Deploy on infra-deployments + uses: aurelien-baudet/workflow-dispatch@v2 + with: + workflow: Update application single value + repo: huggingface/infra-deployments + wait-for-completion: true + wait-for-completion-interval: 10s + display-workflow-run-url-interval: 10s + ref: refs/heads/main + token: ${{ secrets.GIT_TOKEN_INFRA_DEPLOYMENT }} + inputs: '{"path": "hub/chat-ui/chat-ui.yaml", "value": ${{ env.VALUES }}, "url": "${{ github.event.head_commit.url }}"}' diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml new file mode 100644 index 0000000000000000000000000000000000000000..5b892dd31b439e5d35747ffabb390b61577e1565 --- /dev/null +++ b/.github/workflows/lint-and-test.yml @@ -0,0 +1,52 @@ +name: Lint and test + +on: + pull_request: + push: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "npm" + - run: | + npm install ci + - name: "Checking lint/format errors" + run: | + npm run lint + - name: "Checking type errors" + run: | + npm run check + + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "npm" + - run: | + npm ci + - name: "Tests" + run: | + npm run test + + build-check: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v3 + - name: Build Docker image + run: docker build --secret id=DOTENV_LOCAL,src=.env.ci -t chat-ui:latest . diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml new file mode 100644 index 0000000000000000000000000000000000000000..bd49d7cc03d279637692f699308398a09388b7be --- /dev/null +++ b/.github/workflows/trufflehog.yml @@ -0,0 +1,17 @@ +on: + push: + +name: Secret Leaks + +jobs: + trufflehog: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Secret Scanning + uses: trufflesecurity/trufflehog@main + with: + extra_args: --results=verified,unknown diff --git a/.github/workflows/upload-pr-documentation.yml b/.github/workflows/upload-pr-documentation.yml new file mode 100644 index 0000000000000000000000000000000000000000..091d9423e04d18e8afc55836edec9c16556e32c0 --- /dev/null +++ b/.github/workflows/upload-pr-documentation.yml @@ -0,0 +1,16 @@ +name: Upload PR Documentation + +on: + workflow_run: + workflows: ["Build PR Documentation"] + types: + - completed + +jobs: + build: + uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@main + with: + package_name: chat-ui + secrets: + hf_token: ${{ secrets.HF_DOC_BUILD_PUSH }} + comment_bot_token: ${{ secrets.COMMENT_BOT_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..dcd5b727cc3526c3a7081b8677eb79b564d54999 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +SECRET_CONFIG +.idea +!.env.ci +!.env +gcp-*.json +db \ No newline at end of file diff --git a/.husky/lint-stage-config.js b/.husky/lint-stage-config.js new file mode 100644 index 0000000000000000000000000000000000000000..abab8885bcc6f9aa09b83f762c77b75ed8fd3e8b --- /dev/null +++ b/.husky/lint-stage-config.js @@ -0,0 +1,4 @@ +export default { + "*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix", "eslint"], + "*.json": ["prettier --write"], +}; diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000000000000000000000000000000000000..4d9467a4abbaa51e20bfb0238b7df3c466a0cb91 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +set -e +npx lint-staged --config ./.husky/lint-stage-config.js diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000000000000000000000000000000000..b6f27f135954640c8cc5bfd7b8c9922ca6eb2aad --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000000000000000000000000000000000..177a4e072adf23e4836601b8d113f6adf1c411a8 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,14 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +/chart +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..de36577e274217d824839b69b08e04a130509732 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "useTabs": true, + "trailingComma": "es5", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..52a466a5b80955092c9df3a0f128003683757d5f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "command": "npm run dev", + "name": "Run development server", + "request": "launch", + "type": "node-terminal" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..4a65d631069b9e5b3c6c00438d29f9eaa4efb80e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + }, + "eslint.validate": ["javascript", "svelte"], + "[svelte]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..7f1dbb47ca48cd12b7fca294ce79ecb9691c4fc3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,95 @@ +# syntax=docker/dockerfile:1 +ARG INCLUDE_DB=false + +FROM node:20-slim AS base +ENV PLAYWRIGHT_SKIP_BROWSER_GC=1 + +# install dotenv-cli +RUN npm install -g dotenv-cli + +# switch to a user that works for spaces +RUN userdel -r node +RUN useradd -m -u 1000 user +USER user + +ENV HOME=/home/user \ + PATH=/home/user/.local/bin:$PATH + +WORKDIR /app + +# add a .env.local if the user doesn't bind a volume to it +RUN touch /app/.env.local + + +RUN npm i --no-package-lock --no-save playwright@1.47.0 +USER root +RUN apt-get update +RUN apt-get install gnupg curl -y +RUN npx playwright install --with-deps chromium +RUN chown -R 1000:1000 /home/user/.npm +USER user + +COPY --chown=1000 .env /app/.env +COPY --chown=1000 entrypoint.sh /app/entrypoint.sh +COPY --chown=1000 gcp-*.json /app/ +COPY --chown=1000 package.json /app/package.json +COPY --chown=1000 package-lock.json /app/package-lock.json + +RUN chmod +x /app/entrypoint.sh + + +FROM node:20 AS builder + +WORKDIR /app + +COPY --link --chown=1000 package-lock.json package.json ./ + +ARG APP_BASE= +ARG PUBLIC_APP_COLOR=blue +ENV BODY_SIZE_LIMIT=15728640 + +RUN --mount=type=cache,target=/app/.npm \ + npm set cache /app/.npm && \ + npm ci + +COPY --link --chown=1000 . . + +RUN git config --global --add safe.directory /app && \ + npm run build + +# mongo image +FROM mongo:7 AS mongo + +# image to be used if INCLUDE_DB is false +FROM base AS local_db_false + +# image to be used if INCLUDE_DB is true +FROM base AS local_db_true + +# copy mongo from the other stage +COPY --from=mongo /usr/bin/mongo* /usr/bin/ + +ENV MONGODB_URL=mongodb://localhost:27017 +USER root +RUN mkdir -p /data/db +RUN chown -R 1000:1000 /data/db +USER user +# final image +FROM local_db_${INCLUDE_DB} AS final + +# build arg to determine if the database should be included +ARG INCLUDE_DB=false +ENV INCLUDE_DB=${INCLUDE_DB} + +# svelte requires APP_BASE at build time so it must be passed as a build arg +ARG APP_BASE= +# tailwind requires the primary theme to be known at build time so it must be passed as a build arg +ARG PUBLIC_APP_COLOR=blue +ARG PUBLIC_COMMIT_SHA= +ENV PUBLIC_COMMIT_SHA=${PUBLIC_COMMIT_SHA} +ENV BODY_SIZE_LIMIT=15728640 +#import the build & dependencies +COPY --from=builder --chown=1000 /app/build /app/build +COPY --from=builder --chown=1000 /app/node_modules /app/node_modules + +CMD ["/bin/bash", "-c", "/app/entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e44d8f5b79a0643c99977835611e1da9d08fc3cf --- /dev/null +++ b/LICENSE @@ -0,0 +1,203 @@ +Copyright 2018- The Hugging Face team. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000000000000000000000000000000000000..587676d77e169bb9eeaa66688143e98d74acd889 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,35 @@ +## Privacy + +> Last updated: Feb 14, 2025 + +Users of HuggingChat are authenticated through their HF user account. + +We endorse Privacy by Design. As such, your conversations are private to you and will not be shared with anyone, including model authors, for any purpose, including for research or model training purposes. + +You conversation data will only be stored to let you access past conversations. You can click on the Delete icon to delete any past conversation at any moment. + +🗓 Please also consult huggingface.co's main privacy policy at . To exercise any of your legal privacy rights, please send an email to . + +## About available LLMs + +The goal of this app is to showcase that it is now possible to build an open source alternative to ChatGPT. 💪 + +We aim to always provide a diverse set of state of the art open LLMs, hence we rotate the available models over time. Discuss available models and request new ones on the [models discussion page](https://huggingface.co/spaces/huggingchat/chat-ui/discussions/372). + +Check the [models](https://huggingface.co/chat/models/) page for an up-to-date list of the best available LLMs. + +## Technical details + +[![chat-ui](https://img.shields.io/github/stars/huggingface/chat-ui)](https://github.com/huggingface/chat-ui) + +The app is completely open source, and further development takes place on the [huggingface/chat-ui](https://github.com/huggingface/chat-ui) GitHub repo. We're always open to contributions! + +You can find the production configuration for HuggingChat [here](https://github.com/huggingface/chat-ui/blob/main/chart/env/prod.yaml). + +The inference backend is running the optimized [text-generation-inference](https://github.com/huggingface/text-generation-inference) on HuggingFace's Inference API infrastructure. + +It is possible to deploy a copy of this app to a Space and customize it (swap model, add some UI elements, or store user messages according to your own Terms and conditions). You can also 1-click deploy your own instance using the [Chat UI Spaces Docker template](https://huggingface.co/new-space?template=huggingchat/chat-ui-template). + +We welcome any feedback on this app: please participate to the public discussion at + + diff --git a/PROMPTS.md b/PROMPTS.md new file mode 100644 index 0000000000000000000000000000000000000000..643e4bd3d9b1bb1220a4b241ac9c4b6233cf9219 --- /dev/null +++ b/PROMPTS.md @@ -0,0 +1,72 @@ +# Prompt templates + +> [!WARNING] +> We now recommend using the `tokenizer` field to get the chat template directly from the hub. Just set it to your model id on the hub to automatically get the template. + +These are the templates used to format the conversation history for different models used in HuggingChat. Set them in your `.env.local` [like so](https://github.com/huggingface/chat-ui#chatprompttemplate). + +## Llama 2 + +```env +[INST] <>\n{{preprompt}}\n<>\n\n{{#each messages}}{{#ifUser}}{{content}} [/INST] {{/ifUser}}{{#ifAssistant}}{{content}} [INST] {{/ifAssistant}}{{/each}} +``` + +## CodeLlama + +```env +[INST] <>\n{{preprompt}}\n<>\n\n{{#each messages}}{{#ifUser}}{{content}} [/INST] {{/ifUser}}{{#ifAssistant}}{{content}} [INST] {{/ifAssistant}}{{/each}} +``` + +## Falcon + +```env +System: {{preprompt}}\nUser:{{#each messages}}{{#ifUser}}{{content}}\nFalcon:{{/ifUser}}{{#ifAssistant}}{{content}}\nUser:{{/ifAssistant}}{{/each}} +``` + +## Mistral + +```env +{{#each messages}}{{#ifUser}}[INST] {{#if @first}}{{#if @root.preprompt}}{{@root.preprompt}}\n{{/if}}{{/if}} {{content}} [/INST]{{/ifUser}}{{#ifAssistant}}{{content}} {{/ifAssistant}}{{/each}} +``` + +## Zephyr + +```env +<|system|>\n{{preprompt}}\n{{#each messages}}{{#ifUser}}<|user|>\n{{content}}\n<|assistant|>\n{{/ifUser}}{{#ifAssistant}}{{content}}\n{{/ifAssistant}}{{/each}} +``` + +## IDEFICS + +```env +{{#each messages}}{{#ifUser}}User: {{content}}{{/ifUser}}\nAssistant: {{#ifAssistant}}{{content}}\n{{/ifAssistant}}{{/each}} +``` + +## OpenChat + +```env +{{#each messages}}{{#ifUser}}GPT4 User: {{#if @first}}{{#if @root.preprompt}}{{@root.preprompt}}\n{{/if}}{{/if}}{{content}}<|end_of_turn|>GPT4 Assistant: {{/ifUser}}{{#ifAssistant}}{{content}}<|end_of_turn|>{{/ifAssistant}}{{/each}} +``` + +## Mixtral + +```env + {{#each messages}}{{#ifUser}}[INST]{{#if @first}}{{#if @root.preprompt}}{{@root.preprompt}}\n{{/if}}{{/if}} {{content}} [/INST]{{/ifUser}}{{#ifAssistant}} {{content}} {{/ifAssistant}}{{/each}} +``` + +## ChatML + +```env +{{#if @root.preprompt}}<|im_start|>system\n{{@root.preprompt}}<|im_end|>\n{{/if}}{{#each messages}}{{#ifUser}}<|im_start|>user\n{{content}}<|im_end|>\n<|im_start|>assistant\n{{/ifUser}}{{#ifAssistant}}{{content}}<|im_end|>\n{{/ifAssistant}}{{/each}} +``` + +## CodeLlama 70B + +```env +{{#if @root.preprompt}}Source: system\n\n {{@root.preprompt}} {{/if}}{{#each messages}}{{#ifUser}}Source: user\n\n {{content}} {{/ifUser}}{{#ifAssistant}}Source: assistant\n\n {{content}} {{/ifAssistant}}{{/each}}Source: assistant\nDestination: user\n\n `` +``` + +## Gemma + +```env +{{#each messages}}{{#ifUser}}user\n{{#if @first}}{{#if @root.preprompt}}{{@root.preprompt}}\n{{/if}}{{/if}}{{content}}\nmodel\n{{/ifUser}}{{#ifAssistant}}{{content}}\n{{/ifAssistant}}{{/each}} +``` diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..477bcc088c304e4430f02b91670c914bf82dd0d8 --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: chat-ui +version: 0.0.1-latest +type: application +icon: https://huggingface.co/front/assets/huggingface_logo-noborder.svg diff --git a/chart/env/prod.yaml b/chart/env/prod.yaml new file mode 100644 index 0000000000000000000000000000000000000000..05d870d3e3fec911be0324e22bb29d63d91af7ab --- /dev/null +++ b/chart/env/prod.yaml @@ -0,0 +1,677 @@ +image: + repository: huggingface + name: chat-ui + +nodeSelector: + role-huggingchat: "true" + +tolerations: + - key: "huggingface.co/huggingchat" + operator: "Equal" + value: "true" + effect: "NoSchedule" + +serviceAccount: + enabled: true + create: true + name: huggingchat-prod + +ingress: + path: "/chat" + annotations: + alb.ingress.kubernetes.io/healthcheck-path: "/healthcheck" + alb.ingress.kubernetes.io/listen-ports: "[{\"HTTP\": 80}, {\"HTTPS\": 443}]" + alb.ingress.kubernetes.io/group.name: "hub-prod" + alb.ingress.kubernetes.io/scheme: "internet-facing" + alb.ingress.kubernetes.io/ssl-redirect: "443" + alb.ingress.kubernetes.io/tags: "Env=prod,Project=hub,Terraform=true" + alb.ingress.kubernetes.io/target-node-labels: "role-hub-utils=true" + kubernetes.io/ingress.class: "alb" + +envVars: + ADDRESS_HEADER: 'X-Forwarded-For' + ALTERNATIVE_REDIRECT_URLS: '["huggingchat://login/callback"]' + APP_BASE: "/chat" + ALLOW_IFRAME: "false" + COMMUNITY_TOOLS: "true" + COOKIE_SAMESITE: "lax" + COOKIE_SECURE: "true" + ENABLE_ASSISTANTS: "true" + ENABLE_ASSISTANTS_RAG: "true" + METRICS_PORT: 5565 + LOG_LEVEL: "debug" + METRICS_ENABLED: "true" + MODELS: > + [ + { + "name": "meta-llama/Llama-3.3-70B-Instruct", + "id": "meta-llama/Llama-3.3-70B-Instruct", + "description": "Ideal for everyday use. A fast and extremely capable model matching closed source models' capabilities. Now with the latest Llama 3.3 weights!", + "modelUrl": "https://huggingface.co/meta-llama/Llama-3.3-70B-Instruct", + "websiteUrl": "https://llama.meta.com/", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/meta-logo.png", + "tools": true, + "preprompt": "", + "parameters": { + "stop": ["<|endoftext|>", "<|eot_id|>"], + "temperature": 0.6, + "max_new_tokens": 1024, + "truncate": 7167 + }, + "promptExamples": [ + { + "title": "Write an email from bullet list", + "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)" + }, + { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + }, + { + "title": "Assist in a task", + "prompt": "How do I make a delicious lemon cheesecake?" + } + ] + }, + { + "name": "Qwen/Qwen2.5-72B-Instruct", + "description": "The latest Qwen open model with improved role-playing, long text generation and structured data understanding.", + "modelUrl": "https://huggingface.co/Qwen/Qwen2.5-72B-Instruct", + "websiteUrl": "https://qwenlm.github.io/blog/qwen2.5/", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/qwen-logo.png", + "preprompt": "You are Qwen, created by Alibaba Cloud. You are a helpful assistant.", + "parameters": { + "stop": ["<|endoftext|>", "<|im_end|>"], + "temperature": 0.6, + "truncate": 28672, + "max_new_tokens": 3072 + }, + "tools": true, + "promptExamples": [ + { + "title": "Write an email from bullet list", + "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)" + }, + { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + }, + { + "title": "Assist in a task", + "prompt": "How do I make a delicious lemon cheesecake?" + } + ] + }, + { + "name": "CohereForAI/c4ai-command-r-plus-08-2024", + "description": "Cohere's largest language model, optimized for conversational interaction and tool use. Now with the 2024 update!", + "modelUrl": "https://huggingface.co/CohereForAI/c4ai-command-r-plus-08-2024", + "websiteUrl": "https://docs.cohere.com/docs/command-r-plus", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/cohere-logo.png", + "tools": true, + "parameters": { + "stop": ["<|END_OF_TURN_TOKEN|>", "<|im_end|>"], + "truncate": 28672, + "max_new_tokens": 2048, + "temperature": 0.3 + }, + "promptExamples": [ + { + "title": "Generate a mouse portrait", + "prompt": "Generate the portrait of a scientific mouse in its laboratory." + }, + { + "title": "Review a pull request", + "prompt": "Review this pull request: https://github.com/huggingface/chat-ui/pull/1131/files" + }, + { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + } + ] + }, + { + "name": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", + "modelUrl": "https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", + "websiteUrl": "https://deepseek.com/", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/deepseek-logo.png", + "description": "The first reasoning model from DeepSeek, distilled into a 32B dense model. Outperforms o1-mini on multiple benchmarks.", + "reasoning": { + "type": "tokens", + "beginToken": "", + "endToken": "" + }, + "promptExamples": [ + { + "title": "Rs in strawberry", + "prompt": "how many R in strawberry?" + }, + { + "title": "Larger number", + "prompt": "9.11 or 9.9 which number is larger?" + }, + { + "title": "Measuring 6 liters", + "prompt": "I have a 6- and a 12-liter jug. I want to measure exactly 6 liters." + } + ], + "endpoints": [ + { + "type": "openai", + "baseURL": "https://internal.api-inference.huggingface.co/models/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B/v1" + } + ] + }, + { + "name": "nvidia/Llama-3.1-Nemotron-70B-Instruct-HF", + "modelUrl": "https://huggingface.co/nvidia/Llama-3.1-Nemotron-70B-Instruct-HF", + "websiteUrl": "https://www.nvidia.com/", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/nvidia-logo.png", + "description": "Nvidia's latest Llama fine-tune, topping alignment benchmarks and optimized for instruction following.", + "parameters": { + "stop": ["<|eot_id|>", "<|im_end|>"], + "temperature": 0.5, + "truncate": 28672, + "max_new_tokens": 2048 + }, + "promptExamples": [ + { + "title": "Rs in strawberry", + "prompt": "how many R in strawberry?" + }, + { + "title": "Larger number", + "prompt": "9.11 or 9.9 which number is larger?" + }, + { + "title": "Measuring 6 liters", + "prompt": "I have a 6- and a 12-liter jug. I want to measure exactly 6 liters." + } + ], + "endpoints": [ + { + "type": "openai", + "baseURL": "https://internal.api-inference.huggingface.co/models/nvidia/Llama-3.1-Nemotron-70B-Instruct-HF/v1" + } + ] + }, + { + "name": "Qwen/QwQ-32B", + "preprompt": "You are a helpful and harmless assistant. You are Qwen developed by Alibaba. You should think step-by-step.", + "modelUrl": "https://huggingface.co/Qwen/QwQ-32B", + "websiteUrl": "https://qwenlm.github.io/blog/qwq-32b/", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/qwen-logo.png", + "description": "QwQ is the latest reasoning model released by the Qwen team, approaching the capabilities of R1 in benchmarks.", + "reasoning": { + "type": "tokens", + "beginToken": "", + "endToken": "" + }, + "promptExamples": [ + { + "title": "Rs in strawberry", + "prompt": "how many R in strawberry?" + }, + { + "title": "Larger number", + "prompt": "9.11 or 9.9 which number is larger?" + }, + { + "title": "Measuring 6 liters", + "prompt": "I have a 6- and a 12-liter jug. I want to measure exactly 6 liters." + } + ], + "endpoints": [ + { + "type": "openai", + "baseURL": "https://atv7xs1nxxtx2wl0.us-east-1.aws.endpoints.huggingface.cloud/v1" + } + ] + }, + { + "name": "Qwen/Qwen2.5-Coder-32B-Instruct", + "description": "Qwen's latest coding model, in its biggest size yet. SOTA on many coding benchmarks.", + "modelUrl": "https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct", + "websiteUrl": "https://qwenlm.github.io/blog/qwen2.5-coder-family/", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/qwen-logo.png", + "preprompt": "You are Qwen, created by Alibaba Cloud. You are a helpful assistant.", + "parameters": { + "stop": ["<|im_end|>", "<|endoftext|>"], + "temperature": 0.6, + "truncate": 28672, + "max_new_tokens": 3072 + }, + "promptExamples": [ + { + "title": "To-do list web app", + "prompt": "Create a simple to-do list application where users can:\n- Add new tasks.\n- Mark tasks as complete.\n- Delete completed tasks.\nThe tasks should persist in the browser's local storage so that they remain available even after a page reload.\n" + }, + { + "title": "Create a REST API", + "prompt": "Build a simple REST API using Node.js, TypeScript and Express:\n- POST /items: Accepts a JSON body with name and quantity and adds a new item.\n- GET /items: Returns a list of all items.\n- PUT /items/:id: Updates the name or quantity of an item by its id.\n- DELETE /items/:id: Removes an item by its id.\nUse an in-memory array as the data store (no need for a database). Include basic error handling (e.g., item not found)." + }, + { + "title": "Simple website", + "prompt": "Generate a snazzy static landing page for a local coffee shop using HTML and CSS. You can use tailwind using ." + } + ], + "endpoints": [ + { + "type": "openai", + "baseURL": "https://internal.api-inference.huggingface.co/models/Qwen/Qwen2.5-Coder-32B-Instruct/v1" + } + ] + }, + { + "name": "google/gemma-3-27b-it", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/google-logo.png", + "multimodal": true, + "description": "Google's latest open model with great multilingual performance, supports image inputs natively.", + "websiteUrl": "https://blog.google/technology/developers/gemma-3/", + "promptExamples": [ + { + "title": "Write an email from bullet list", + "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)" + }, + { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + }, + { + "title": "Assist in a task", + "prompt": "How do I make a delicious lemon cheesecake?" + } + ], + "endpoints": [ + { + "type": "openai", + "baseURL": "https://wp0d3hn6s3k8jk22.us-east-1.aws.endpoints.huggingface.cloud/v1", + "multimodal": { + "image": { + "maxSizeInMB": 10, + "maxWidth": 560, + "maxHeight": 560, + "supportedMimeTypes": ["image/jpeg"], + "preferredMimeType": "image/jpeg" + } + } + } + ] + }, + { + "name": "meta-llama/Llama-3.2-11B-Vision-Instruct", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/meta-logo.png", + "description": "The latest multimodal model from Meta! Supports image inputs natively.", + "websiteUrl": "https://llama.com/", + "multimodal": true, + "parameters": { + "stop": ["<|eot_id|>", "<|im_end|>"], + "temperature": 0.6, + "truncate": 14336, + "max_new_tokens": 1536 + }, + "promptExamples": [ + { + "title": "Write an email from bullet list", + "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)" + }, + { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + }, + { + "title": "Assist in a task", + "prompt": "How do I make a delicious lemon cheesecake?" + } + ], + "endpoints": [ + { + "type": "openai", + "baseURL": "https://internal.api-inference.huggingface.co/models/meta-llama/Llama-3.2-11B-Vision-Instruct/v1", + "multimodal": { + "image": { + "maxSizeInMB": 10, + "maxWidth": 560, + "maxHeight": 560, + "supportedMimeTypes": ["image/png", "image/jpeg", "image/webp"], + "preferredMimeType": "image/webp" + } + } + } + ] + }, + { + "name": "NousResearch/Hermes-3-Llama-3.1-8B", + "description": "Nous Research's latest Hermes 3 release in 8B size. Follows instruction closely.", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/nous-logo.png", + "websiteUrl": "https://nousresearch.com/", + "modelUrl": "https://huggingface.co/NousResearch/Hermes-3-Llama-3.1-8B", + "promptExamples": [ + { + "title": "Write an email from bullet list", + "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)" + }, + { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + }, + { + "title": "Assist in a task", + "prompt": "How do I make a delicious lemon cheesecake?" + } + ], + "parameters": { + "stop": ["<|im_end|>"], + "temperature": 0.6, + "truncate": 14336, + "max_new_tokens": 1536 + } + }, + { + "name": "mistralai/Mistral-Nemo-Instruct-2407", + "displayName": "mistralai/Mistral-Nemo-Instruct-2407", + "description": "A small model with good capabilities in language understanding and commonsense reasoning.", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/mistral-logo.png", + "websiteUrl": "https://mistral.ai/news/mistral-nemo/", + "modelUrl": "https://huggingface.co/mistralai/Mistral-Nemo-Instruct-2407", + "preprompt": "", + "parameters": { + "stop": [""], + "temperature": 0.6, + "truncate": 14336, + "max_new_tokens": 1536 + }, + "promptExamples": [ + { + "title": "Write an email from bullet list", + "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)" + }, + { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + }, + { + "title": "Assist in a task", + "prompt": "How do I make a delicious lemon cheesecake?" + } + ] + }, + { + "name": "microsoft/Phi-3.5-mini-instruct", + "description": "One of the best small models (3.8B parameters), super fast for simple tasks.", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/microsoft-logo.png", + "modelUrl": "https://huggingface.co/microsoft/Phi-3.5-mini-instruct", + "websiteUrl": "https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/discover-the-new-multi-lingual-high-quality-phi-3-5-slms/ba-p/4225280/", + "preprompt": "", + "parameters": { + "stop": ["<|end|>", "<|endoftext|>", "<|assistant|>"], + "temperature": 0.6, + "truncate": 28672, + "max_new_tokens": 3072 + }, + "promptExamples": [ + { + "title": "Write an email from bullet list", + "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)" + }, + { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + }, + { + "title": "Assist in a task", + "prompt": "How do I make a delicious lemon cheesecake?" + } + ] + }, + { + "name": "internal/task", + "tokenizer" : "NousResearch/Hermes-3-Llama-3.1-8B", + "unlisted": true, + "tools" : true, + "endpoints": [ + { + "type": "openai", + "baseURL": "https://internal.api-inference.huggingface.co/models/NousResearch/Hermes-3-Llama-3.1-8B/v1" + } + ], + "parameters": { + "temperature": 0.1, + "max_new_tokens": 256 + }, + } + ] + + NODE_ENV: "prod" + NODE_LOG_STRUCTURED_DATA: true + OLD_MODELS: > + [ + { "name": "bigcode/starcoder" }, + { "name": "OpenAssistant/oasst-sft-6-llama-30b-xor" }, + { "name": "HuggingFaceH4/zephyr-7b-alpha" }, + { "name": "openchat/openchat_3.5" }, + { "name": "openchat/openchat-3.5-1210" }, + { "name": "tiiuae/falcon-180B-chat" }, + { "name": "codellama/CodeLlama-34b-Instruct-hf" }, + { "name": "google/gemma-7b-it" }, + { "name": "meta-llama/Llama-2-70b-chat-hf" }, + { "name": "codellama/CodeLlama-70b-Instruct-hf" }, + { "name": "openchat/openchat-3.5-0106" }, + { "name": "meta-llama/Meta-Llama-3-70B-Instruct" }, + { "name": "meta-llama/Meta-Llama-3.1-405B-Instruct-FP8" }, + { + "name": "CohereForAI/c4ai-command-r-plus", + "transferTo": "CohereForAI/c4ai-command-r-plus-08-2024" + }, + { + "name": "01-ai/Yi-1.5-34B-Chat", + "transferTo": "CohereForAI/c4ai-command-r-plus-08-2024" + }, + { + "name": "mistralai/Mixtral-8x7B-Instruct-v0.1", + "transferTo": "mistralai/Mistral-Nemo-Instruct-2407" + }, + { + "name": "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO", + "transferTo": "NousResearch/Hermes-3-Llama-3.1-8B" + }, + { + "name": "mistralai/Mistral-7B-Instruct-v0.3", + "transferTo": "mistralai/Mistral-Nemo-Instruct-2407" + }, + { + "name": "microsoft/Phi-3-mini-4k-instruct", + "transferTo": "microsoft/Phi-3.5-mini-instruct" + }, + { + "name": "meta-llama/Meta-Llama-3.1-70B-Instruct", + "transferTo": "meta-llama/Llama-3.3-70B-Instruct" + }, + { + "name": "Qwen/QwQ-32B-Preview", + "transferTo": "Qwen/QwQ-32B" + } + ] + PUBLIC_ORIGIN: "https://huggingface.co" + PUBLIC_SHARE_PREFIX: "https://hf.co/chat" + PUBLIC_ANNOUNCEMENT_BANNERS: > + [ + { + "title": "DeepSeek R1 is now available!", + "linkTitle": "Try it out!", + "linkHref": "https://huggingface.co/chat/models/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B" + } + ] + PUBLIC_APP_NAME: "HuggingChat" + PUBLIC_APP_ASSETS: "huggingchat" + PUBLIC_APP_COLOR: "yellow" + PUBLIC_APP_DESCRIPTION: "Making the community's best AI chat models available to everyone." + PUBLIC_APP_DISCLAIMER_MESSAGE: "Disclaimer: AI is an area of active research with known problems such as biased generation and misinformation. Do not use this application for high-stakes decisions or advice." + PUBLIC_APP_GUEST_MESSAGE: "Sign in with a free Hugging Face account to continue using HuggingChat." + PUBLIC_APP_DATA_SHARING: 0 + PUBLIC_APP_DISCLAIMER: 1 + PUBLIC_PLAUSIBLE_SCRIPT_URL: "/js/script.js" + REQUIRE_FEATURED_ASSISTANTS: "true" + TASK_MODEL: "internal/task" + TEXT_EMBEDDING_MODELS: > + [{ + "name": "bge-base-en-v1-5-sxa", + "displayName": "bge-base-en-v1-5-sxa", + "chunkCharLength": 512, + "endpoints": [{ + "type": "tei", + "url": "https://huggingchat-tei.hf.space/" + }] + }] + WEBSEARCH_BLOCKLIST: '["youtube.com", "twitter.com"]' + XFF_DEPTH: '2' + TOOLS: > + [ + { + "_id": "000000000000000000000001", + "displayName": "Image Generation", + "description": "Use this tool to generate images based on a prompt.", + "color": "yellow", + "icon": "camera", + "baseUrl": "black-forest-labs/FLUX.1-schnell", + "name": "image_generation", + "endpoint": "/infer", + "inputs": [ + { + "name": "prompt", + "description": "A prompt to generate an image from", + "paramType": "required", + "type": "str" + }, + { "name": "seed", "paramType": "fixed", "value": "0", "type": "float" }, + { + "name": "randomize_seed", + "paramType": "fixed", + "value": "true", + "type": "bool" + }, + { + "name": "width", + "description": "numeric value between 256 and 2048", + "paramType": "optional", + "default": 1024, + "type": "float" + }, + { + "name": "height", + "description": "numeric value between 256 and 2048", + "paramType": "optional", + "default": 1024, + "type": "float" + }, + { + "name": "num_inference_steps", + "paramType": "fixed", + "value": "4", + "type": "float" + } + ], + "outputComponent": "image", + "outputComponentIdx": 0, + "showOutput": true + }, + { + "_id": "000000000000000000000002", + "displayName": "Document Parser", + "description": "Use this tool to parse any document and get its content in markdown format.", + "color": "yellow", + "icon": "cloud", + "baseUrl": "huggingchat/document-parser", + "name": "document_parser", + "endpoint": "/predict", + "inputs": [ + { + "name": "document", + "description": "Filename of the document to parse", + "paramType": "required", + "type": "file", + "mimeTypes": 'application/*' + }, + { + "name": "filename", + "paramType": "fixed", + "value": "document.pdf", + "type": "str" + } + ], + "outputComponent": "textbox", + "outputComponentIdx": 0, + "showOutput": false, + "isHidden": true + }, + { + "_id": "000000000000000000000003", + "name": "edit_image", + "baseUrl": "multimodalart/cosxl", + "endpoint": "/run_edit", + "inputs": [ + { + "name": "image", + "description": "The image path to be edited", + "paramType": "required", + "type": "file", + "mimeTypes": 'image/*' + }, + { + "name": "prompt", + "description": "The prompt with which to edit the image", + "paramType": "required", + "type": "str" + }, + { + "name": "negative_prompt", + "paramType": "fixed", + "value": "", + "type": "str" + }, + { + "name": "guidance_scale", + "paramType": "fixed", + "value": 6.5, + "type": "float" + }, + { + "name": "steps", + "paramType": "fixed", + "value": 30, + "type": "float" + } + ], + "outputComponent": "image", + "showOutput": true, + "displayName": "Image Editor", + "color": "green", + "icon": "camera", + "description": "This tool lets you edit images", + "outputComponentIdx": 0 + } + ] + HF_ORG_ADMIN: '644171cfbd0c97265298aa99' + HF_ORG_EARLY_ACCESS: '5e67bd5b1009063689407478' + HF_API_ROOT: 'https://internal.api-inference.huggingface.co/models' +infisical: + enabled: true + env: "prod-us-east-1" + +autoscaling: + enabled: true + minReplicas: 12 + maxReplicas: 30 + targetMemoryUtilizationPercentage: "50" + targetCPUUtilizationPercentage: "50" + +resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 4 + memory: 8Gi + +monitoring: + enabled: true diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 0000000000000000000000000000000000000000..eee5a181d225c2aff53344c446288240d37d3d0b --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,22 @@ +{{- define "name" -}} +{{- default $.Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "app.name" -}} +chat-ui +{{- end -}} + +{{- define "labels.standard" -}} +release: {{ $.Release.Name | quote }} +heritage: {{ $.Release.Service | quote }} +chart: "{{ include "name" . }}" +app: "{{ include "app.name" . }}" +{{- end -}} + +{{- define "labels.resolver" -}} +release: {{ $.Release.Name | quote }} +heritage: {{ $.Release.Service | quote }} +chart: "{{ include "name" . }}" +app: "{{ include "app.name" . }}-resolver" +{{- end -}} + diff --git a/chart/templates/config.yaml b/chart/templates/config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c4c803e9e5f8b473ae216d5a2933cb67d46bc011 --- /dev/null +++ b/chart/templates/config.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + labels: {{ include "labels.standard" . | nindent 4 }} + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} +data: + {{- range $key, $value := $.Values.envVars }} + {{ $key }}: {{ $value | quote }} + {{- end }} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a9f8d562265ee2bf549e1102adfd2f301665056a --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: {{ include "labels.standard" . | nindent 4 }} + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} + {{- if .Values.infisical.enabled }} + annotations: + secrets.infisical.com/auto-reload: "true" + {{- end }} +spec: + progressDeadlineSeconds: 600 + {{- if not $.Values.autoscaling.enabled }} + replicas: {{ .Values.replicas }} + {{- end }} + revisionHistoryLimit: 10 + selector: + matchLabels: {{ include "labels.standard" . | nindent 6 }} + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: {{ include "labels.standard" . | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/config.yaml") . | sha256sum }} + {{- if $.Values.envVars.NODE_LOG_STRUCTURED_DATA }} + co.elastic.logs/json.expand_keys: "true" + {{- end }} + spec: + {{- if .Values.serviceAccount.enabled }} + serviceAccountName: "{{ .Values.serviceAccount.name | default (include "name" .) }}" + {{- end }} + containers: + - name: chat-ui + image: "{{ .Values.image.repository }}/{{ .Values.image.name }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + readinessProbe: + failureThreshold: 30 + periodSeconds: 10 + httpGet: + path: {{ $.Values.envVars.APP_BASE | default "" }}/healthcheck + port: {{ $.Values.envVars.APP_PORT | default 3000 | int }} + livenessProbe: + failureThreshold: 30 + periodSeconds: 10 + httpGet: + path: {{ $.Values.envVars.APP_BASE | default "" }}/healthcheck + port: {{ $.Values.envVars.APP_PORT | default 3000 | int }} + ports: + - containerPort: {{ $.Values.envVars.APP_PORT | default 3000 | int }} + name: http + protocol: TCP + {{- if $.Values.monitoring.enabled }} + - containerPort: {{ $.Values.envVars.METRICS_PORT | default 5565 | int }} + name: metrics + protocol: TCP + {{- end }} + resources: {{ toYaml .Values.resources | nindent 12 }} + {{- with $.Values.extraEnv }} + env: + {{- toYaml . | nindent 14 }} + {{- end }} + envFrom: + - configMapRef: + name: {{ include "name" . }} + {{- if $.Values.infisical.enabled }} + - secretRef: + name: {{ include "name" $ }}-secs + {{- end }} + {{- with $.Values.extraEnvFrom }} + {{- toYaml . | nindent 14 }} + {{- end }} + nodeSelector: {{ toYaml .Values.nodeSelector | nindent 8 }} + tolerations: {{ toYaml .Values.tolerations | nindent 8 }} + volumes: + - name: config + configMap: + name: {{ include "name" . }} diff --git a/chart/templates/hpa.yaml b/chart/templates/hpa.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bf7bd3b256b79c54269ae39afb02c816878596dc --- /dev/null +++ b/chart/templates/hpa.yaml @@ -0,0 +1,45 @@ +{{- if $.Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: {{ include "labels.standard" . | nindent 4 }} + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "name" . }} + minReplicas: {{ $.Values.autoscaling.minReplicas }} + maxReplicas: {{ $.Values.autoscaling.maxReplicas }} + metrics: + {{- if ne "" $.Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ $.Values.autoscaling.targetMemoryUtilizationPercentage | int }} + {{- end }} + {{- if ne "" $.Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ $.Values.autoscaling.targetCPUUtilizationPercentage | int }} + {{- end }} + behavior: + scaleDown: + stabilizationWindowSeconds: 600 + policies: + - type: Percent + value: 10 + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 0 + policies: + - type: Pods + value: 1 + periodSeconds: 30 +{{- end }} diff --git a/chart/templates/infisical.yaml b/chart/templates/infisical.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6a11e084f6e0ab300ea4ec2b2694a79dadc1bdf8 --- /dev/null +++ b/chart/templates/infisical.yaml @@ -0,0 +1,24 @@ +{{- if .Values.infisical.enabled }} +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: {{ include "name" $ }}-infisical-secret + namespace: {{ $.Release.Namespace }} +spec: + authentication: + universalAuth: + credentialsRef: + secretName: {{ .Values.infisical.operatorSecretName | quote }} + secretNamespace: {{ .Values.infisical.operatorSecretNamespace | quote }} + secretsScope: + envSlug: {{ .Values.infisical.env | quote }} + projectSlug: {{ .Values.infisical.project | quote }} + secretsPath: / + hostAPI: {{ .Values.infisical.url | quote }} + managedSecretReference: + creationPolicy: Owner + secretName: {{ include "name" $ }}-secs + secretNamespace: {{ .Release.Namespace | quote }} + secretType: Opaque + resyncInterval: {{ .Values.infisical.resyncInterval }} +{{- end }} diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8ba4e8a4055f23471b77597dcfc956dc811547e0 --- /dev/null +++ b/chart/templates/ingress.yaml @@ -0,0 +1,32 @@ +{{- if $.Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: {{ toYaml .Values.ingress.annotations | nindent 4 }} + labels: {{ include "labels.standard" . | nindent 4 }} + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} +spec: + {{ if $.Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{ end }} + {{- with .Values.ingress.tls }} + tls: + - hosts: + - {{ $.Values.domain | quote }} + {{- with .secretName }} + secretName: {{ . }} + {{- end }} + {{- end }} + rules: + - host: {{ .Values.domain }} + http: + paths: + - backend: + service: + name: {{ include "name" . }} + port: + name: http + path: {{ $.Values.ingress.path | default "/" }} + pathType: Prefix +{{- end }} diff --git a/chart/templates/network-policy.yaml b/chart/templates/network-policy.yaml new file mode 100644 index 0000000000000000000000000000000000000000..59f5df5893a97f4075237ac7cdb4979dce7298a9 --- /dev/null +++ b/chart/templates/network-policy.yaml @@ -0,0 +1,36 @@ +{{- if $.Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} +spec: + egress: + - ports: + - port: 53 + protocol: UDP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + - to: + {{- range $ip := .Values.networkPolicy.allowedBlocks }} + - ipBlock: + cidr: {{ $ip | quote }} + {{- end }} + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - 169.254.169.254/32 + podSelector: + matchLabels: {{ include "labels.standard" . | nindent 6 }} + policyTypes: + - Egress +{{- end }} diff --git a/chart/templates/service-account.yaml b/chart/templates/service-account.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fc3a184c9def4cef61836f8eac152ab61fe4d047 --- /dev/null +++ b/chart/templates/service-account.yaml @@ -0,0 +1,13 @@ +{{- if and .Values.serviceAccount.enabled .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +metadata: + name: "{{ .Values.serviceAccount.name | default (include "name" .) }}" + namespace: {{ .Release.Namespace }} + labels: {{ include "labels.standard" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/chart/templates/service-monitor.yaml b/chart/templates/service-monitor.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2d7fdae07d839b37ee0032ebb2b895b85af80a9b --- /dev/null +++ b/chart/templates/service-monitor.yaml @@ -0,0 +1,15 @@ +{{- if $.Values.monitoring.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: {{ include "labels.standard" . | nindent 4 }} + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} +spec: + selector: + matchLabels: {{ include "labels.standard" . | nindent 6 }} + endpoints: + - port: metrics + path: /metrics + interval: 15s +{{- end }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..682f8571fa5b1e9b2532660c5e61a30ccc715e2c --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{{ include "name" . }}" + annotations: {{ toYaml .Values.service.annotations | nindent 4 }} + namespace: {{ .Release.Namespace }} + labels: {{ include "labels.standard" . | nindent 4 }} +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + {{- if $.Values.monitoring.enabled }} + - name: metrics + port: 5565 + protocol: TCP + targetPort: metrics + {{- end }} + selector: {{ include "labels.standard" . | nindent 4 }} + type: {{.Values.service.type}} diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b55ba1a04ccc150aecb642a7cf745327d35f8df8 --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,67 @@ +image: + repository: ghcr.io/huggingface + name: chat-ui + tag: 0.0.0-latest + pullPolicy: IfNotPresent + +replicas: 3 + +domain: huggingface.co + +networkPolicy: + enabled: false + allowedBlocks: [] + +service: + type: NodePort + annotations: { } + +serviceAccount: + enabled: false + create: false + name: "" + automountServiceAccountToken: true + annotations: { } + +ingress: + enabled: true + path: "/" + annotations: { } + # className: "nginx" + tls: { } + # secretName: XXX + +resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 2 + memory: 4Gi +nodeSelector: {} +tolerations: [] + +envVars: { } + +infisical: + enabled: false + env: "" + project: "huggingchat-v2-a1" + url: "" + resyncInterval: 60 + operatorSecretName: "huggingchat-operator-secrets" + operatorSecretNamespace: "hub-utils" + +# Allow to environment injections on top or instead of infisical +extraEnvFrom: [] +extraEnv: [] + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 2 + targetMemoryUtilizationPercentage: "" + targetCPUUtilizationPercentage: "" + +monitoring: + enabled: false diff --git a/docs/source/_toctree.yml b/docs/source/_toctree.yml new file mode 100644 index 0000000000000000000000000000000000000000..6287065386e44a96b8bfec3b75c7572eef98b06d --- /dev/null +++ b/docs/source/_toctree.yml @@ -0,0 +1,64 @@ +- local: index + title: 🤗 Chat UI +- title: Installation + sections: + - local: installation/local + title: Local + - local: installation/spaces + title: Spaces + - local: installation/docker + title: Docker + - local: installation/helm + title: Helm +- title: Configuration + sections: + - local: configuration/overview + title: Overview + - local: configuration/theming + title: Theming + - local: configuration/open-id + title: OpenID + - local: configuration/web-search + title: Web Search + - local: configuration/metrics + title: Metrics + - local: configuration/embeddings + title: Text Embedding Models + - title: Models + sections: + - local: configuration/models/overview + title: Overview + - local: configuration/models/multimodal + title: Multimodal + - local: configuration/models/tools + title: Tools + - title: Providers + sections: + - local: configuration/models/providers/anthropic + title: Anthropic + - local: configuration/models/providers/aws + title: AWS + - local: configuration/models/providers/cloudflare + title: Cloudflare + - local: configuration/models/providers/cohere + title: Cohere + - local: configuration/models/providers/google + title: Google + - local: configuration/models/providers/langserve + title: Langserve + - local: configuration/models/providers/llamacpp + title: Llama.cpp + - local: configuration/models/providers/ollama + title: Ollama + - local: configuration/models/providers/openai + title: OpenAI + - local: configuration/models/providers/tgi + title: TGI + - local: configuration/common-issues + title: Common Issues +- title: Developing + sections: + - local: developing/architecture + title: Architecture + - local: developing/copy-huggingchat + title: Copy HuggingChat diff --git a/docs/source/configuration/common-issues.md b/docs/source/configuration/common-issues.md new file mode 100644 index 0000000000000000000000000000000000000000..8ebb98fff0e7e8311fe81946d87f4b8b36a77ca1 --- /dev/null +++ b/docs/source/configuration/common-issues.md @@ -0,0 +1,7 @@ +# Common Issues + +## 403:You don't have access to this conversation + +Most likely you are running chat-ui over HTTP. The recommended option is to setup something like NGINX to handle HTTPS and proxy the requests to chat-ui. If you really need to run over HTTP you can add `ALLOW_INSECURE_COOKIES=true` to your `.env.local`. + +Make sure to set your `PUBLIC_ORIGIN` in your `.env.local` to the correct URL as well. diff --git a/docs/source/configuration/embeddings.md b/docs/source/configuration/embeddings.md new file mode 100644 index 0000000000000000000000000000000000000000..ad853725ca1809dfea778592029969e7f156cbb4 --- /dev/null +++ b/docs/source/configuration/embeddings.md @@ -0,0 +1,105 @@ +# Text Embedding Models + +By default (for backward compatibility), when `TEXT_EMBEDDING_MODELS` environment variable is not defined, [transformers.js](https://huggingface.co/docs/transformers.js) embedding models will be used for embedding tasks, specifically, the [Xenova/gte-small](https://huggingface.co/Xenova/gte-small) model. + +You can customize the embedding model by setting `TEXT_EMBEDDING_MODELS` in your `.env.local` file where the required fields are `name`, `chunkCharLength` and `endpoints`. + +Supported text embedding backends are: [`transformers.js`](https://huggingface.co/docs/transformers.js), [`TEI`](https://github.com/huggingface/text-embeddings-inference) and [`OpenAI`](https://platform.openai.com/docs/guides/embeddings). `transformers.js` models run locally as part of `chat-ui`, whereas `TEI` models run in a different environment & accessed through an API endpoint. `openai` models are accessed through the [OpenAI API](https://platform.openai.com/docs/guides/embeddings). + +When more than one embedding models are supplied in `.env.local` file, the first will be used by default, and the others will only be used on LLM's which configured `embeddingModel` to the name of the model. + +## Transformers.js + +The Transformers.js backend uses local CPU for the embedding which can be quite slow. If possible, consider using TEI or OpenAI embeddings instead if you use web search frequently, as performance will improve significantly. + +```ini +TEXT_EMBEDDING_MODELS = `[ + { + "name": "Xenova/gte-small", + "displayName": "Xenova/gte-small", + "description": "locally running embedding", + "chunkCharLength": 512, + "endpoints": [ + { "type": "transformersjs" } + ] + } +]` +``` + +## Text Embeddings Inference (TEI) + +> Text Embeddings Inference (TEI) is a comprehensive toolkit designed for efficient deployment and serving of open source text embeddings models. It enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE, and E5. + +Some recommended models at the time of writing (May 2024) are `Snowflake/snowflake-arctic-embed-m` and `BAAI/bge-large-en-v1.5`. You may run TEI locally with GPU support via Docker: + +`docker run --gpus all -p 8080:80 -v tei-data:/data --name tei ghcr.io/huggingface/text-embeddings-inference:1.2 --model-id YOUR/HF_MODEL` + +You can then hook this up to your Chat UI instance with the following configuration. + +```ini +TEXT_EMBEDDING_MODELS=`[ + { + "name": "YOUR/HF_MODEL", + "displayName": "YOUR/HF_MODEL", + "preQuery": "Check the model documentation for the preQuery. Not all models have one", + "prePassage": "Check the model documentation for the prePassage. Not all models have one", + "chunkCharLength": 512, + "endpoints": [{ + "type": "tei", + "url": "http://127.0.0.1:8080/" + }] + } +]` +``` + +Examples for `Snowflake/snowflake-arctic-embed-m` and `BAAI/bge-large-en-v1.5`: + +```ini +TEXT_EMBEDDING_MODELS=`[ + { + "name": "Snowflake/snowflake-arctic-embed-m", + "displayName": "Snowflake/snowflake-arctic-embed-m", + "preQuery": "Represent this sentence for searching relevant passages: ", + "chunkCharLength": 512, + "endpoints": [{ + "type": "tei", + "url": "http://127.0.0.1:8080/" + }] + },{ + "name": "BAAI/bge-large-en-v1.5", + "displayName": "BAAI/bge-large-en-v1.5", + "chunkCharLength": 512, + "endpoints": [{ + "type": "tei", + "url": "http://127.0.0.1:8080/" + }] + } +]` +``` + +## OpenAI + +It's also possible to host your own OpenAI API compatible embedding models. [`Infinity`](https://github.com/michaelfeil/infinity) is one example. You may run it locally with Docker: + +`docker run -it --gpus all -v infinity-data:/app/.cache -p 7997:7997 michaelf34/infinity:latest v2 --model-id nomic-ai/nomic-embed-text-v1 --port 7997` + +You can then hook this up to your Chat UI instance with the following configuration. + +```ini +TEXT_EMBEDDING_MODELS=`[ + { + "name": "nomic-ai/nomic-embed-text-v1", + "displayName": "nomic-ai/nomic-embed-text-v1", + "chunkCharLength": 512, + "model": { + "name": "nomic-ai/nomic-embed-text-v1" + }, + "endpoints": [ + { + "type": "openai", + "url": "https://127.0.0.1:7997/embeddings" + } + ] + } +]` +``` diff --git a/docs/source/configuration/metrics.md b/docs/source/configuration/metrics.md new file mode 100644 index 0000000000000000000000000000000000000000..45ad3e368baafd4a7bc8411eef2cf135335e2c6c --- /dev/null +++ b/docs/source/configuration/metrics.md @@ -0,0 +1,9 @@ +# Metrics + +The server can expose prometheus metrics on port `5565` but is off by default. You may enable the metrics server with `METRICS_ENABLED=true` and change the port with `METRICS_PORT=1234`. + + + +In development with `npm run dev`, the metrics server does not shutdown gracefully due to Sveltekit not providing hooks for restart. It's recommended to disable the metrics server in this case. + + diff --git a/docs/source/configuration/models/multimodal.md b/docs/source/configuration/models/multimodal.md new file mode 100644 index 0000000000000000000000000000000000000000..a8f1b367269f559a01ca82927eeb48b8bd9a1410 --- /dev/null +++ b/docs/source/configuration/models/multimodal.md @@ -0,0 +1,24 @@ +# Multimodal + +We currently support [IDEFICS](https://huggingface.co/blog/idefics) (hosted on [TGI](./providers/tgi)), OpenAI and Anthropic Claude 3 as multimodal models. You can enable it by setting `multimodal: true` in your `MODELS` configuration. For IDEFICS, you must have a [PRO HF Api token](https://huggingface.co/settings/tokens). For OpenAI, see the [OpenAI section](./providers/openai). For Anthropic, see the [Anthropic section](./providers/anthropic). + +```ini +MODELS=`[ + { + "name": "HuggingFaceM4/idefics-80b-instruct", + "multimodal" : true, + "description": "IDEFICS is the new multimodal model by Hugging Face.", + "preprompt": "", + "chatPromptTemplate" : "{{#each messages}}{{#ifUser}}User: {{content}}{{/ifUser}}\nAssistant: {{#ifAssistant}}{{content}}\n{{/ifAssistant}}{{/each}}", + "parameters": { + "temperature": 0.1, + "top_p": 0.95, + "repetition_penalty": 1.2, + "top_k": 12, + "truncate": 1000, + "max_new_tokens": 1024, + "stop": ["", "User:", "\nUser:"] + } + } +]` +``` diff --git a/docs/source/configuration/models/overview.md b/docs/source/configuration/models/overview.md new file mode 100644 index 0000000000000000000000000000000000000000..0a458646de2547d5c53696ceb05856d214c01282 --- /dev/null +++ b/docs/source/configuration/models/overview.md @@ -0,0 +1,147 @@ +# Models Overview + +You can customize the parameters passed to the model or even use a new model by updating the `MODELS` variable in your `.env.local`. The default one can be found in `.env` and looks like this : + +```ini +MODELS=`[ + { + "name": "mistralai/Mistral-7B-Instruct-v0.2", + "displayName": "mistralai/Mistral-7B-Instruct-v0.2", + "description": "Mistral 7B is a new Apache 2.0 model, released by Mistral AI that outperforms Llama2 13B in benchmarks.", + "websiteUrl": "https://mistral.ai/news/announcing-mistral-7b/", + "preprompt": "", + "chatPromptTemplate" : "{{#each messages}}{{#ifUser}}[INST] {{#if @first}}{{#if @root.preprompt}}{{@root.preprompt}}\n{{/if}}{{/if}}{{content}} [/INST]{{/ifUser}}{{#ifAssistant}}{{content}}{{/ifAssistant}}{{/each}}", + "parameters": { + "temperature": 0.3, + "top_p": 0.95, + "repetition_penalty": 1.2, + "top_k": 50, + "truncate": 3072, + "max_new_tokens": 1024, + "stop": [""] + }, + "promptExamples": [ + { + "title": "Write an email from bullet list", + "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)" + }, { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + }, { + "title": "Assist in a task", + "prompt": "How do I make a delicious lemon cheesecake?" + } + ] + } +]` + +``` + +You can change things like the parameters, or customize the preprompt to better suit your needs. You can also add more models by adding more objects to the array, with different preprompts for example. + +## Chat Prompt Template + +When querying the model for a chat response, the `chatPromptTemplate` template is used. `messages` is an array of chat messages, it has the format `[{ content: string }, ...]`. To identify if a message is a user message or an assistant message the `ifUser` and `ifAssistant` block helpers can be used. + +The following is the default `chatPromptTemplate`, although newlines and indentiation have been added for readability. You can find the prompts used in production for HuggingChat [here](https://github.com/huggingface/chat-ui/blob/main/PROMPTS.md). The templating language used is [Handlebars](https://www.npmjs.com/package/handlebars). + +```handlebars +{{preprompt}} +{{#each messages}} + {{#ifUser}}{{@root.userMessageToken}}{{content}}{{@root.userMessageEndToken}}{{/ifUser}} + {{#ifAssistant + }}{{@root.assistantMessageToken}}{{content}}{{@root.assistantMessageEndToken}}{{/ifAssistant}} +{{/each}} +{{assistantMessageToken}} +``` + +## Custom endpoint authorization + +### Basic and Bearer + +Custom endpoints may require authorization, depending on how you configure them. Authentication will usually be set either with `Basic` or `Bearer`. + +For `Basic` we will need to generate a base64 encoding of the username and password. + +`echo -n "USER:PASS" | base64` + +> VVNFUjpQQVNT + +For `Bearer` you can use a token, which can be grabbed from [here](https://huggingface.co/settings/tokens). + +You can then add the generated information and the `authorization` parameter to your `.env.local`. + +```ini +"endpoints": [ + { + "url": "https://HOST:PORT", + "authorization": "Basic VVNFUjpQQVNT", + } +] +``` + +Please note that if `HF_TOKEN` is also set or not empty, it will take precedence. + +## Models hosted on multiple custom endpoints + +If the model being hosted will be available on multiple servers/instances add the `weight` parameter to your `.env.local`. The `weight` will be used to determine the probability of requesting a particular endpoint. + +```ini +"endpoints": [ + { + "url": "https://HOST:PORT", + "weight": 1 + }, + { + "url": "https://HOST:PORT", + "weight": 2 + } + ... +] +``` + +## Client Certificate Authentication (mTLS) + +Custom endpoints may require client certificate authentication, depending on how you configure them. To enable mTLS between Chat UI and your custom endpoint, you will need to set the `USE_CLIENT_CERTIFICATE` to `true`, and add the `CERT_PATH` and `KEY_PATH` parameters to your `.env.local`. These parameters should point to the location of the certificate and key files on your local machine. The certificate and key files should be in PEM format. The key file can be encrypted with a passphrase, in which case you will also need to add the `CLIENT_KEY_PASSWORD` parameter to your `.env.local`. + +If you're using a certificate signed by a private CA, you will also need to add the `CA_PATH` parameter to your `.env.local`. This parameter should point to the location of the CA certificate file on your local machine. + +If you're using a self-signed certificate, e.g. for testing or development purposes, you can set the `REJECT_UNAUTHORIZED` parameter to `false` in your `.env.local`. This will disable certificate validation, and allow Chat UI to connect to your custom endpoint. + +## Specific Embedding Model + +A model can use any of the embedding models defined under `TEXT_EMBEDDING_MODELS`, (currently used when web searching). By default it will use the first embedding model, but it can be changed with the field `embeddingModel`: + +```ini +TEXT_EMBEDDING_MODELS = `[ + { + "name": "Xenova/gte-small", + "chunkCharLength": 512, + "endpoints": [ + {"type": "transformersjs"} + ] + }, + { + "name": "intfloat/e5-base-v2", + "chunkCharLength": 768, + "endpoints": [ + {"type": "tei", "url": "http://127.0.0.1:8080/", "authorization": "Basic VVNFUjpQQVNT"}, + {"type": "tei", "url": "http://127.0.0.1:8081/"} + ] + } +]` + +MODELS=`[ + { + "name": "Ollama Mistral", + "chatPromptTemplate": "...", + "embeddingModel": "intfloat/e5-base-v2" + "parameters": { + ... + }, + "endpoints": [ + ... + ] + } +]` +``` diff --git a/docs/source/configuration/models/providers/anthropic.md b/docs/source/configuration/models/providers/anthropic.md new file mode 100644 index 0000000000000000000000000000000000000000..db18bc12ab55d420d8f3050cd8de0937523a43b0 --- /dev/null +++ b/docs/source/configuration/models/providers/anthropic.md @@ -0,0 +1,117 @@ +# Anthropic + +| Feature | Available | +| --------------------------- | --------- | +| [Tools](../tools) | No | +| [Multimodal](../multimodal) | Yes | + +We also support Anthropic models (including multimodal ones via `multmodal: true`) through the official SDK. You may provide your API key via the `ANTHROPIC_API_KEY` env variable, or alternatively, through the `endpoints.apiKey` as per the following example. + +```ini +MODELS=`[ + { + "name": "claude-3-haiku-20240307", + "displayName": "Claude 3 Haiku", + "description": "Fastest and most compact model for near-instant responsiveness", + "multimodal": true, + "parameters": { + "max_new_tokens": 4096, + }, + "endpoints": [ + { + "type": "anthropic", + // optionals + "apiKey": "sk-ant-...", + "baseURL": "https://api.anthropic.com", + "defaultHeaders": {}, + "defaultQuery": {} + } + ] + }, + { + "name": "claude-3-sonnet-20240229", + "displayName": "Claude 3 Sonnet", + "description": "Ideal balance of intelligence and speed", + "multimodal": true, + "parameters": { + "max_new_tokens": 4096, + }, + "endpoints": [ + { + "type": "anthropic", + // optionals + "apiKey": "sk-ant-...", + "baseURL": "https://api.anthropic.com", + "defaultHeaders": {}, + "defaultQuery": {} + } + ] + }, + { + "name": "claude-3-opus-20240229", + "displayName": "Claude 3 Opus", + "description": "Most powerful model for highly complex tasks", + "multimodal": true, + "parameters": { + "max_new_tokens": 4096 + }, + "endpoints": [ + { + "type": "anthropic", + // optionals + "apiKey": "sk-ant-...", + "baseURL": "https://api.anthropic.com", + "defaultHeaders": {}, + "defaultQuery": {} + } + ] + } +]` +``` + +## VertexAI + +We also support using Anthropic models running on Vertex AI. Authentication is done using Google Application Default Credentials. Project ID can be provided through the `endpoints.projectId` as per the following example: + +```ini +MODELS=`[ + { + "name": "claude-3-haiku@20240307", + "displayName": "Claude 3 Haiku", + "description": "Fastest, most compact model for near-instant responsiveness", + "multimodal": true, + "parameters": { + "max_new_tokens": 4096 + }, + "endpoints": [ + { + "type": "anthropic-vertex", + "region": "us-central1", + "projectId": "gcp-project-id", + // optionals + "defaultHeaders": {}, + "defaultQuery": {} + } + ] + }, + { + "name": "claude-3-sonnet@20240229", + "displayName": "Claude 3 Sonnet", + "description": "Ideal balance of intelligence and speed", + "multimodal": true, + "parameters": { + "max_new_tokens": 4096, + }, + "endpoints": [ + { + "type": "anthropic-vertex", + "region": "us-central1", + "projectId": "gcp-project-id", + // optionals + "defaultHeaders": {}, + "defaultQuery": {} + } + ] + }, +]` +``` diff --git a/docs/source/configuration/models/providers/aws.md b/docs/source/configuration/models/providers/aws.md new file mode 100644 index 0000000000000000000000000000000000000000..07374de45f30566a0425365b621d236ffd648f46 --- /dev/null +++ b/docs/source/configuration/models/providers/aws.md @@ -0,0 +1,35 @@ +# Amazon Web Services (AWS) + +| Feature | Available | +| --------------------------- | --------- | +| [Tools](../tools) | No | +| [Multimodal](../multimodal) | No | + +You may specify your Amazon SageMaker instance as an endpoint for Chat UI: + +```ini +MODELS=`[{ + "name": "your-model", + "displayName": "Your Model", + "description": "Your description", + "parameters": { + "max_new_tokens": 4096 + }, + "endpoints": [ + { + "type" : "aws", + "service" : "sagemaker" + "url": "", + "accessKey": "", + "secretKey" : "", + "sessionToken": "", + "region": "", + "weight": 1 + } + ] +}]` +``` + +You can also set `"service": "lambda"` to use a lambda instance. + +You can get the `accessKey` and `secretKey` from your AWS user, under programmatic access. diff --git a/docs/source/configuration/models/providers/cloudflare.md b/docs/source/configuration/models/providers/cloudflare.md new file mode 100644 index 0000000000000000000000000000000000000000..43567f3c16a75a47013eb9a395d038222d931d10 --- /dev/null +++ b/docs/source/configuration/models/providers/cloudflare.md @@ -0,0 +1,35 @@ +# Cloudflare + +| Feature | Available | +| --------------------------- | --------- | +| [Tools](../tools) | No | +| [Multimodal](../multimodal) | No | + +You may use Cloudflare Workers AI to run your own models with serverless inference. + +You will need to have a Cloudflare account, then get your [account ID](https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/) as well as your [API token](https://developers.cloudflare.com/workers-ai/get-started/rest-api/#1-get-an-api-token) for Workers AI. + +You can either specify them directly in your `.env.local` using the `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` variables, or you can set them directly in the endpoint config. + +You can find the list of models available on Cloudflare [here](https://developers.cloudflare.com/workers-ai/models/#text-generation). + +```ini +MODELS=`[ + { + "name" : "nousresearch/hermes-2-pro-mistral-7b", + "tokenizer": "nousresearch/hermes-2-pro-mistral-7b", + "parameters": { + "stop": ["<|im_end|>"] + }, + "endpoints" : [ + { + "type" : "cloudflare" + + } + ] + } +]` +``` diff --git a/docs/source/configuration/models/providers/cohere.md b/docs/source/configuration/models/providers/cohere.md new file mode 100644 index 0000000000000000000000000000000000000000..6def938d41f8235aa59e2352927e438ac592df38 --- /dev/null +++ b/docs/source/configuration/models/providers/cohere.md @@ -0,0 +1,26 @@ +# Cohere + +| Feature | Available | +| --------------------------- | --------- | +| [Tools](../tools) | Yes | +| [Multimodal](../multimodal) | No | + +You may use Cohere to run their models directly from Chat UI. You will need to have a Cohere account, then get your [API token](https://dashboard.cohere.com/api-keys). You can either specify it directly in your `.env.local` using the `COHERE_API_TOKEN` variable, or you can set it in the endpoint config. + +Here is an example of a Cohere model config. You can set which model you want to use by setting the `id` field to the model name. + +```ini +MODELS=`[ + { + "name": "command-r-plus", + "displayName": "Command R+", + "tools": true, + "endpoints": [{ + "type": "cohere", + + }] + } +]` +``` diff --git a/docs/source/configuration/models/providers/google.md b/docs/source/configuration/models/providers/google.md new file mode 100644 index 0000000000000000000000000000000000000000..1d8dcd33f3b46d9f8bb3e739575a28ba52ef667f --- /dev/null +++ b/docs/source/configuration/models/providers/google.md @@ -0,0 +1,92 @@ +# Google + +| Feature | Available | +| --------------------------- | --------- | +| [Tools](../tools) | No | +| [Multimodal](../multimodal) | No | + +Chat UI can connect to the google Vertex API endpoints ([List of supported models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models)). + +To enable: + +1. [Select](https://console.cloud.google.com/project) or [create](https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project) a Google Cloud project. +1. [Enable billing for your project](https://cloud.google.com/billing/docs/how-to/modify-project). +1. [Enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com). +1. [Set up authentication with a service account](https://cloud.google.com/docs/authentication/getting-started) + so you can access the API from your local workstation. + +The service account credentials file can be imported as an environmental variable: + +```ini +GOOGLE_APPLICATION_CREDENTIALS = clientid.json +``` + +Make sure your docker container has access to the file and the variable is correctly set. +Afterwards Google Vertex endpoints can be configured as following: + +```ini +MODELS=`[ + { + "name": "gemini-1.5-pro", + "displayName": "Vertex Gemini Pro 1.5", + "endpoints" : [{ + "type": "vertex", + "project": "abc-xyz", + "location": "europe-west3", + "extraBody": { + "model_version": "gemini-1.5-pro-002", + }, + // Optional + "safetyThreshold": "BLOCK_MEDIUM_AND_ABOVE", + "apiEndpoint": "", // alternative api endpoint url, + "tools": [{ + "googleSearchRetrieval": { + "disableAttribution": true + } + }] + }] + } +]` +``` + +## GenAI + +Or use the Gemini API API provider [from](https://github.com/google-gemini/generative-ai-js#readme): + +Make sure that you have an API key from Google Cloud Platform. To get an API key, follow the instructions [here](https://ai.google.dev/gemini-api/docs/api-key). + +You can either specify them directly in your `.env.local` using the `GOOGLE_GENAI_API_KEY` variables, or you can set them directly in the endpoint config. + +You can find the list of models available [here](https://ai.google.dev/gemini-api/docs/models/gemini), and experimental models available [here](https://ai.google.dev/gemini-api/docs/models/experimental-models). + +```ini +MODELS=`[ + { + "name": "gemini-1.5-flash", + "displayName": "Gemini Flash 1.5", + "multimodal": true, + "endpoints": [ + { + "type": "genai", + + // Optional + "apiKey": "abc...xyz" + "safetyThreshold": "BLOCK_MEDIUM_AND_ABOVE", + } + ] + }, + { + "name": "gemini-1.5-pro", + "displayName": "Gemini Pro 1.5", + "multimodal": false, + "endpoints": [ + { + "type": "genai", + + // Optional + "apiKey": "abc...xyz" + } + ] + } +]` +``` diff --git a/docs/source/configuration/models/providers/langserve.md b/docs/source/configuration/models/providers/langserve.md new file mode 100644 index 0000000000000000000000000000000000000000..931358c716ecbdfb9d3cbce05e7ebdeb30318e3b --- /dev/null +++ b/docs/source/configuration/models/providers/langserve.md @@ -0,0 +1,22 @@ +# LangServe + +| Feature | Available | +| --------------------------- | --------- | +| [Tools](../tools) | No | +| [Multimodal](../multimodal) | No | + +LangChain applications that are deployed using LangServe can be called with the following config: + +```ini +MODELS=`[ + { + "name": "summarization-chain", + "displayName": "Summarization Chain" + "endpoints" : [{ + "type": "langserve", + "url" : "http://127.0.0.1:8100", + }] + } +]` + +``` diff --git a/docs/source/configuration/models/providers/llamacpp.md b/docs/source/configuration/models/providers/llamacpp.md new file mode 100644 index 0000000000000000000000000000000000000000..5dfcc175ec9f0929afef971e4cac38e9daf2db00 --- /dev/null +++ b/docs/source/configuration/models/providers/llamacpp.md @@ -0,0 +1,49 @@ +# Llama.cpp + +| Feature | Available | +| --------------------------- | --------- | +| [Tools](../tools) | No | +| [Multimodal](../multimodal) | No | + +Chat UI supports the llama.cpp API server directly without the need for an adapter. You can do this using the `llamacpp` endpoint type. + +If you want to run Chat UI with llama.cpp, you can do the following, using [microsoft/Phi-3-mini-4k-instruct-gguf](https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf) as an example model: + +```bash +# install llama.cpp +brew install llama.cpp +# start llama.cpp server +llama-server --hf-repo microsoft/Phi-3-mini-4k-instruct-gguf --hf-file Phi-3-mini-4k-instruct-q4.gguf -c 4096 +``` + +_note: you can swap the `hf-repo` and `hf-file` with your fav GGUF on the [Hub](https://huggingface.co/models?library=gguf). For example: `--hf-repo TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF` for [this repo](https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF) & `--hf-file tinyllama-1.1b-chat-v1.0.Q4_0.gguf` for [this file](https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/blob/main/tinyllama-1.1b-chat-v1.0.Q4_0.gguf)._ + +A local LLaMA.cpp HTTP Server will start on `http://localhost:8080` (to change the port or any other default options, please find [LLaMA.cpp HTTP Server readme](https://github.com/ggerganov/llama.cpp/tree/master/examples/server)). + +Add the following to your `.env.local`: + +```ini +MODELS=`[ + { + "name": "Local microsoft/Phi-3-mini-4k-instruct-gguf", + "tokenizer": "microsoft/Phi-3-mini-4k-instruct-gguf", + "preprompt": "", + "chatPromptTemplate": "{{preprompt}}{{#each messages}}{{#ifUser}}<|user|>\n{{content}}<|end|>\n<|assistant|>\n{{/ifUser}}{{#ifAssistant}}{{content}}<|end|>\n{{/ifAssistant}}{{/each}}", + "parameters": { + "stop": ["<|end|>", "<|endoftext|>", "<|assistant|>"], + "temperature": 0.7, + "max_new_tokens": 1024, + "truncate": 3071 + }, + "endpoints": [{ + "type" : "llamacpp", + "baseURL": "http://localhost:8080" + }], + }, +]` +``` + +
+ + +
diff --git a/docs/source/configuration/models/providers/ollama.md b/docs/source/configuration/models/providers/ollama.md new file mode 100644 index 0000000000000000000000000000000000000000..ae0f4f189dc26f601c24aebcaef68c0d2facc0ae --- /dev/null +++ b/docs/source/configuration/models/providers/ollama.md @@ -0,0 +1,39 @@ +# Ollama + +| Feature | Available | +| --------------------------- | --------- | +| [Tools](../tools) | No | +| [Multimodal](../multimodal) | No | + +We also support the Ollama inference server. Spin up a model with + +```bash +ollama run mistral +``` + +Then specify the endpoints like so: + +```ini +MODELS=`[ + { + "name": "Ollama Mistral", + "chatPromptTemplate": "{{#each messages}}{{#ifUser}}[INST] {{#if @first}}{{#if @root.preprompt}}{{@root.preprompt}}\n{{/if}}{{/if}} {{content}} [/INST]{{/ifUser}}{{#ifAssistant}}{{content}} {{/ifAssistant}}{{/each}}", + "parameters": { + "temperature": 0.1, + "top_p": 0.95, + "repetition_penalty": 1.2, + "top_k": 50, + "truncate": 3072, + "max_new_tokens": 1024, + "stop": ["
"] + }, + "endpoints": [ + { + "type": "ollama", + "url" : "http://127.0.0.1:11434", + "ollamaName" : "mistral" + } + ] + } +]` +``` diff --git a/docs/source/configuration/models/providers/openai.md b/docs/source/configuration/models/providers/openai.md new file mode 100644 index 0000000000000000000000000000000000000000..c324f3419dce3a6fe8d028e11de04ec507fceff5 --- /dev/null +++ b/docs/source/configuration/models/providers/openai.md @@ -0,0 +1,181 @@ +# OpenAI + +| Feature | Available | +| --------------------------- | --------- | +| [Tools](../tools) | No | +| [Multimodal](../multimodal) | Yes | + +Chat UI can be used with any API server that supports OpenAI API compatibility, for example [text-generation-webui](https://github.com/oobabooga/text-generation-webui/tree/main/extensions/openai), [LocalAI](https://github.com/go-skynet/LocalAI), [FastChat](https://github.com/lm-sys/FastChat/blob/main/docs/openai_api.md), [llama-cpp-python](https://github.com/abetlen/llama-cpp-python), and [ialacol](https://github.com/chenhunghan/ialacol) and [vllm](https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html). + +The following example config makes Chat UI works with [text-generation-webui](https://github.com/oobabooga/text-generation-webui/tree/main/extensions/openai), the `endpoint.baseUrl` is the url of the OpenAI API compatible server, this overrides the baseUrl to be used by OpenAI instance. The `endpoint.completion` determine which endpoint to be used, default is `chat_completions` which uses `/chat/completions`, change to `endpoint.completion` to `completions` to use the `/completions` endpoint. + +```ini +MODELS=`[ + { + "name": "text-generation-webui", + "id": "text-generation-webui", + "parameters": { + "temperature": 0.9, + "top_p": 0.95, + "repetition_penalty": 1.2, + "top_k": 50, + "truncate": 1000, + "max_new_tokens": 1024, + "stop": [] + }, + "endpoints": [{ + "type" : "openai", + "baseURL": "http://localhost:8000/v1" + }] + } +]` + +``` + +The `openai` type includes official OpenAI models. You can add, for example, GPT4/GPT3.5 as a "openai" model: + +```ini +OPENAI_API_KEY=#your openai api key here +MODELS=`[{ + "name": "gpt-4", + "displayName": "GPT 4", + "endpoints" : [{ + "type": "openai", + "apiKey": "or your openai api key here" + }] +},{ + "name": "gpt-3.5-turbo", + "displayName": "GPT 3.5 Turbo", + "endpoints" : [{ + "type": "openai", + "apiKey": "or your openai api key here" + }] +}]` +``` + +We also support models in the `o1` family. You need to add a few more options ot the config: Here is an example for `o1-mini`: + +```ini +MODELS=`[ + { + "name": "o1-mini", + "description": "ChatGPT o1-mini", + "systemRoleSupported": false, + "parameters": { + "max_new_tokens": 2048, + }, + "endpoints" : [{ + "type": "openai", + "useCompletionTokens": true, + }] + } +] +``` + +You may also consume any model provider that provides compatible OpenAI API endpoint. For example, you may self-host [Portkey](https://github.com/Portkey-AI/gateway) gateway and experiment with Claude or GPTs offered by Azure OpenAI. Example for Claude from Anthropic: + +```ini +MODELS=`[{ + "name": "claude-2.1", + "displayName": "Claude 2.1", + "description": "Anthropic has been founded by former OpenAI researchers...", + "parameters": { + "temperature": 0.5, + "max_new_tokens": 4096, + }, + "endpoints": [ + { + "type": "openai", + "baseURL": "https://gateway.example.com/v1", + "defaultHeaders": { + "x-portkey-config": '{"provider":"anthropic","api_key":"sk-ant-abc...xyz"}' + } + } + ] +}]` +``` + +Example for GPT 4 deployed on Azure OpenAI: + +```ini +MODELS=`[{ + "id": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview", + "displayName": "gpt-4-1106-preview", + "parameters": { + "temperature": 0.5, + "max_new_tokens": 4096, + }, + "endpoints": [ + { + "type": "openai", + "baseURL": "https://{resource-name}.openai.azure.com/openai/deployments/{deployment-id}", + "defaultHeaders": { + "api-key": "{api-key}" + }, + "defaultQuery": { + "api-version": "2023-05-15" + } + } + ] +}]` +``` + +## DeepInfra + +Or try Mistral from [Deepinfra](https://deepinfra.com/mistralai/Mistral-7B-Instruct-v0.1/api?example=openai-http): + +> Note, apiKey can either be set custom per endpoint, or globally using `OPENAI_API_KEY` variable. + +```ini +MODELS=`[{ + "name": "mistral-7b", + "displayName": "Mistral 7B", + "description": "A 7B dense Transformer, fast-deployed and easily customisable. Small, yet powerful for a variety of use cases. Supports English and code, and a 8k context window.", + "parameters": { + "temperature": 0.5, + "max_new_tokens": 4096, + }, + "endpoints": [ + { + "type": "openai", + "baseURL": "https://api.deepinfra.com/v1/openai", + "apiKey": "abc...xyz" + } + ] +}]` +``` + +_Non-streaming endpoints_ + +For endpoints that don´t support streaming like o1 on Azure, you can pass `streamingSupported: false` in your endpoint config: + +``` +MODELS=`[{ + "id": "o1-preview", + "name": "o1-preview", + "displayName": "o1-preview", + "systemRoleSupported": false, + "endpoints": [ + { + "type": "openai", + "baseURL": "https://my-deployment.openai.azure.com/openai/deployments/o1-preview", + "defaultHeaders": { + "api-key": "$SECRET" + }, + "streamingSupported": false, + } + ] +}]` +``` + +## Other + +Some other providers and their `baseURL` for reference. + +[Groq](https://groq.com/): https://api.groq.com/openai/v1 +[Fireworks](https://fireworks.ai/): https://api.fireworks.ai/inference/v1 + +``` + +``` diff --git a/docs/source/configuration/models/providers/tgi.md b/docs/source/configuration/models/providers/tgi.md new file mode 100644 index 0000000000000000000000000000000000000000..721cf023e9ad80c0cb104ce1b22fee4010e68b55 --- /dev/null +++ b/docs/source/configuration/models/providers/tgi.md @@ -0,0 +1,66 @@ +# Text Generation Inference (TGI) + +| Feature | Available | +| --------------------------- | --------- | +| [Tools](../tools) | Yes\* | +| [Multimodal](../multimodal) | Yes\* | + +\* Tools are only supported with the Cohere Command R+ model with the Xenova tokenizers. Please see the [Tools](../tools) section. + +\* Multimodal is only supported with the IDEFICS model. Please see the [Multimodal](../multimodal) section. + +By default, if `endpoints` are left unspecified, Chat UI will look for the model on the hosted Hugging Face inference API using the model name, and use your `HF_TOKEN`. Refer to the [overview](../overview) for more information about model configuration. + +```ini +MODELS=`[ + { + "name": "mistralai/Mistral-7B-Instruct-v0.2", + "displayName": "mistralai/Mistral-7B-Instruct-v0.2", + "description": "Mistral 7B is a new Apache 2.0 model, released by Mistral AI that outperforms Llama2 13B in benchmarks.", + "websiteUrl": "https://mistral.ai/news/announcing-mistral-7b/", + "preprompt": "", + "chatPromptTemplate" : "{{#each messages}}{{#ifUser}}[INST] {{#if @first}}{{#if @root.preprompt}}{{@root.preprompt}}\n{{/if}}{{/if}}{{content}} [/INST]{{/ifUser}}{{#ifAssistant}}{{content}}{{/ifAssistant}}{{/each}}", + "parameters": { + "temperature": 0.3, + "top_p": 0.95, + "repetition_penalty": 1.2, + "top_k": 50, + "truncate": 3072, + "max_new_tokens": 1024, + "stop": [""] + }, + "promptExamples": [ + { + "title": "Write an email from bullet list", + "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)" + }, { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + }, { + "title": "Assist in a task", + "prompt": "How do I make a delicious lemon cheesecake?" + } + ] + } +]` +``` + +## Running your own models using a custom endpoint + +If you want to, instead of hitting models on the Hugging Face Inference API, you can run your own models locally. + +A good option is to hit a [text-generation-inference](https://github.com/huggingface/text-generation-inference) endpoint. This is what is done in the official [Chat UI Spaces Docker template](https://huggingface.co/new-space?template=huggingchat/chat-ui-template) for instance: both this app and a text-generation-inference server run inside the same container. + +To do this, you can add your own endpoints to the `MODELS` variable in `.env.local`, by adding an `"endpoints"` key for each model in `MODELS`. + +```ini +MODELS=`[{ + "name": "your-model-name", + "displayName": "Your Model Name", + ... other model config + "endpoints": [{ + "type" : "tgi", + "url": "https://HOST:PORT", + }] +}]` +``` diff --git a/docs/source/configuration/models/tools.md b/docs/source/configuration/models/tools.md new file mode 100644 index 0000000000000000000000000000000000000000..b38793640b7b1cd650a5e9e28d89d5fb883332ca --- /dev/null +++ b/docs/source/configuration/models/tools.md @@ -0,0 +1,62 @@ +# Tools + +Tool calling instructs the model to generate an output matching a user-defined schema, which may be parsed for invoking external tools. The model simply chooses the tools and their parameters. Currently, only `TGI` and `Cohere` with `Command R+` are supported. + +
+ + +
+ +## TGI Configuration + +A custom tokenizer is required for prompting the model for generating tool calls, as well as prompting with the results. The expected format for these tools and the resulting tool calls are hard coded for TGI, so it's likely that only the following configuration will work: + +```ini +MODELS=`[ + { + "name" : "CohereForAI/c4ai-command-r-plus", + "displayName": "Command R+", + "description": "Command R+ is Cohere's latest LLM and is the first open weight model to beat GPT4 in the Chatbot Arena!", + "tools": true, + "tokenizer": "Xenova/c4ai-command-r-v01-tokenizer", + "modelUrl": "https://huggingface.co/CohereForAI/c4ai-command-r-plus", + "websiteUrl": "https://docs.cohere.com/docs/command-r-plus", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/cohere-logo.png", + "parameters": { + "stop": ["<|END_OF_TURN_TOKEN|>"], + "truncate" : 28672, + "max_new_tokens" : 4096, + "temperature" : 0.3 + } + } +]` +``` + +## Cohere Configuration + +The Cohere provider supports the endpoint native method of tool calling. Refer to the `endpoints/cohere` for implementation details. + +```ini +MODELS=`[ + { + "name": "command-r-plus", + "displayName": "Command R+", + "description": "Command R+ is Cohere's latest LLM and is the first open weight model to beat GPT4 in the Chatbot Arena!", + "tools": true, + "websiteUrl": "https://docs.cohere.com/docs/command-r-plus", + "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/cohere-logo.png", + "endpoints": [{ + "type": "cohere", + "apiKey": "YOUR_API_KEY" + }] + } +]` +``` + +## Adding Tools + +Tool implementations are placed in `src/lib/server/tools`, with helpers available for easy integration with HuggingFace Zero GPU spaces. In the future, there may be an OpenAPI interface for adding tools. + +## Adding Support for Additional Models + +The TGI implementation uses a custom tokenizer and hard coded schema for supporting tools. The Cohere implementation, on the other hand, uses the native support in the SDK to emit tool calls. This is the recommended way to add support for more models. Please see the `endpoints/cohere` section of the code for implementation details. diff --git a/docs/source/configuration/open-id.md b/docs/source/configuration/open-id.md new file mode 100644 index 0000000000000000000000000000000000000000..90ba2e97a8049150364620d7a80582d14261166e --- /dev/null +++ b/docs/source/configuration/open-id.md @@ -0,0 +1,16 @@ +# OpenID + +The login feature is disabled by default and users are attributed a unique ID based on their browser. But if you want to use OpenID to authenticate your users, you can add the following to your `.env.local` file: + +```ini +OPENID_CONFIG=`{ + PROVIDER_URL: "", + CLIENT_ID: "", + CLIENT_SECRET: "", + SCOPES: "openid profile", + TOLERANCE: // optional + RESOURCE: // optional +}` +``` + +Redirect URI: `/login/callback` diff --git a/docs/source/configuration/overview.md b/docs/source/configuration/overview.md new file mode 100644 index 0000000000000000000000000000000000000000..e20defc99eb4625061be860b897465ec496f0dd1 --- /dev/null +++ b/docs/source/configuration/overview.md @@ -0,0 +1,10 @@ +# Configuration Overview + +Chat UI handles configuration with environment variables. The default config for Chat UI is stored in the `.env` file, which you may use as a reference. You will need to override some values to get Chat UI to run locally. This can be done in `.env.local` or via your environment. The bare minimum configuration to get Chat UI running is: + +```ini +MONGODB_URL=mongodb://localhost:27017 +HF_TOKEN=your_token +``` + +The following sections detail various sections of the app you may want to configure. diff --git a/docs/source/configuration/theming.md b/docs/source/configuration/theming.md new file mode 100644 index 0000000000000000000000000000000000000000..fbd52fed451cf0934045a07c703f24799216f621 --- /dev/null +++ b/docs/source/configuration/theming.md @@ -0,0 +1,18 @@ +# Theming + +You can use a few environment variables to customize the look and feel of Chat UI. These are by default: + +```ini +PUBLIC_APP_NAME=ChatUI +PUBLIC_APP_ASSETS=chatui +PUBLIC_APP_COLOR=blue +PUBLIC_APP_DESCRIPTION="Making the community's best AI chat models available to everyone." +PUBLIC_APP_DATA_SHARING= +PUBLIC_APP_DISCLAIMER= +``` + +- `PUBLIC_APP_NAME` The name used as a title throughout the app. +- `PUBLIC_APP_ASSETS` Is used to find logos & favicons in `static/$PUBLIC_APP_ASSETS`, current options are `chatui` and `huggingchat`. +- `PUBLIC_APP_COLOR` Can be any of the [tailwind colors](https://tailwindcss.com/docs/customizing-colors#default-color-palette). +- `PUBLIC_APP_DATA_SHARING` Can be set to 1 to add a toggle in the user settings that lets your users opt-in to data sharing with models creator. +- `PUBLIC_APP_DISCLAIMER` If set to 1, we show a disclaimer about generated outputs on login. diff --git a/docs/source/configuration/web-search.md b/docs/source/configuration/web-search.md new file mode 100644 index 0000000000000000000000000000000000000000..09d5cc8ed2ed0ed69a87ccad084197b05c7ac437 --- /dev/null +++ b/docs/source/configuration/web-search.md @@ -0,0 +1,58 @@ +# Web Search + +Chat UI features a powerful Web Search feature. A high level overview of how it works: + +1. Generate an appropriate search query from the user prompt using the `TASK_MODEL` +2. Perform web search via an external provider (i.e. Serper) or via locally scrape Google results +3. Load each search result into playwright and scrape +4. Convert scraped HTML to Markdown tree with headings as parents +5. Create embeddings for each Markdown element +6. Find the embedings clossest to the user query using a vector similarity search (inner product) +7. Get the corresponding Markdown elements and their parent, up to 8000 characters +8. Supply the information as context to the model + +
+ + +
+ +## Providers + +Many providers are supported for the web search, or you can use locally scraped Google results. + +### Local + +For locally scraped Google results, put `USE_LOCAL_WEBSEARCH=true` in your `.env.local`. Please note that you may hit rate limits as we make no attempt to make the traffic look legitimate. To avoid this, you may choose a provider, such as Serper, used on the official instance. + +### SearXNG + +> SearXNG is a free internet metasearch engine which aggregates results from various search services and databases. Users are neither tracked nor profiled. + +You may enable support via the `SEARXNG_QUERY_URL` where `` will be replaceed with the query keywords. Please see [the official documentation](https://docs.searxng.org/dev/search_api.html) for more information + +Example: `https://searxng.yourdomain.com/search?q=&engines=duckduckgo,google&format=json` + +### Third Party + +Many third party providers are supported as well. The official instance uses Serper. + +```ini +YDC_API_KEY=docs.you.com api key here +SERPER_API_KEY=serper.dev api key here +SERPAPI_KEY=serpapi key here +SERPSTACK_API_KEY=serpstack api key here +SEARCHAPI_KEY=searchapi api key here +``` + +## Block/Allow List + +You may block or allow specific websites from the web search results. When using an allow list, only the links in the allowlist will be used. For supported search engines, the links will be blocked from the results directly. Any URL in the results that **partially or fully matches** the entry will be filtered out. + +```ini +WEBSEARCH_BLOCKLIST=`["youtube.com", "https://example.com/foo/bar"]` +WEBSEARCH_ALLOWLIST=`["stackoverflow.com"]` +``` + +## Disabling Javascript + +By default, Playwright will execute all Javascript on the page. This can be intensive, requiring up to 6 cores for full performance, on some webpages. You may block scripts from running by settings `WEBSEARCH_JAVASCRIPT=false`. However, this will not block Javascript inlined in the HTML. diff --git a/docs/source/developing/architecture.md b/docs/source/developing/architecture.md new file mode 100644 index 0000000000000000000000000000000000000000..6a41fa16471434ed38dcfa64de2509008524f30b --- /dev/null +++ b/docs/source/developing/architecture.md @@ -0,0 +1,35 @@ +# Architecture + +This document discusses the high level overview of the Chat UI codebase. If you're looking to contribute or just want to understand how the codebase works, this is the place for you! + +## Overview + +Chat UI provides a simple interface connecting LLMs to external information and tools. The project uses [MongoDB](https://www.mongodb.com/) and [SvelteKit](https://kit.svelte.dev/) with [Tailwind](https://tailwindcss.com/). + +## Code Map + +This section discusses various modules of the codebase briefly. The headings are not paths since the codebase structure may change. + +### `routes` + +Provides all of the routes rendered with SSR via SvelteKit. The majority of backend and frontend logic can be found here, with some modules being pulled out into `lib` for the client and `lib/server` for the server. + +### `textGeneration` + +Provides a standard interface for most chat features such as model output, web search, assistants and tools. Outputs `MessageUpdate`s which provide fine-grained updates on the request status such as new tokens and web search results. + +### `endpoints`/`embeddingEndpoints` + +Provides a common streaming interface for many third party LLM and embedding providers. + +### `websearch` + +Implements web search querying and RAG. See the [Web Search](../configuration/web-search) section for more information. + +### `tools` + +Provides a common interface for external tools called by LLMs. See the [Tools](../configuration/models/tools.md) section for more information + +### `migrations` + +Includes all MongoDB migrations for maintaining backwards compatibility across schema changes. Any changes to the schema must include a migration diff --git a/docs/source/developing/copy-huggingchat.md b/docs/source/developing/copy-huggingchat.md new file mode 100644 index 0000000000000000000000000000000000000000..1e64c6f234134f72099bb809a49c34f637a4df56 --- /dev/null +++ b/docs/source/developing/copy-huggingchat.md @@ -0,0 +1,71 @@ +# Copy HuggingChat + +The config file for HuggingChat is stored in the `chart/env/prod.yaml` file. It is the source of truth for the environment variables used for our CI/CD pipeline. For HuggingChat, as we need to customize the app color, as well as the base path, we build a custom docker image. You can find the workflow here. + + + +If you want to make changes to the model config used in production for HuggingChat, you should do so against `chart/env/prod.yaml`. + + + +### Running a copy of HuggingChat locally + +If you want to run an exact copy of HuggingChat locally, you will need to do the following first: + +1. Create an [OAuth App on the hub](https://huggingface.co/settings/applications/new) with `openid profile email` permissions. Make sure to set the callback URL to something like `http://localhost:5173/chat/login/callback` which matches the right path for your local instance. +2. Create a [HF Token](https://huggingface.co/settings/tokens) with your Hugging Face account. You will need a Pro account to be able to access some of the larger models available through HuggingChat. +3. Create a free account with [serper.dev](https://serper.dev/) (you will get 2500 free search queries) +4. Run an instance of MongoDB, however you want. (Local or remote) + +You can then create a new `.env.SECRET_CONFIG` file with the following content + +```ini +MONGODB_URL= +HF_TOKEN= +OPENID_CONFIG=`{ + PROVIDER_URL: "https://huggingface.co", + CLIENT_ID: "", + CLIENT_SECRET: "", +}` +SERPER_API_KEY= +MESSAGES_BEFORE_LOGIN= +``` + +You can then run `npm run updateLocalEnv` in the root of chat-ui. This will create a `.env.local` file which combines the `chart/env/prod.yaml` and the `.env.SECRET_CONFIG` file. You can then run `npm run dev` to start your local instance of HuggingChat. + +### Populate database + + + +The `MONGODB_URL` used for this script will be fetched from `.env.local`. Make sure it's correct! The command runs directly on the database. + + + +You can populate the database using faker data using the `populate` script: + +```bash +npm run populate +``` + +At least one flag must be specified, the following flags are available: + +- `reset` - resets the database +- `all` - populates all tables +- `users` - populates the users table +- `settings` - populates the settings table for existing users +- `assistants` - populates the assistants table for existing users +- `conversations` - populates the conversations table for existing users + +For example, you could use it like so: + +```bash +npm run populate reset +``` + +to clear out the database. Then login in the app to create your user and run the following command: + +```bash +npm run populate users settings assistants conversations +``` + +to populate the database with fake data, including fake conversations and assistants for your user. diff --git a/docs/source/index.md b/docs/source/index.md new file mode 100644 index 0000000000000000000000000000000000000000..d0ba841aa8d70c42ac605c25ef1f25e5c892b58a --- /dev/null +++ b/docs/source/index.md @@ -0,0 +1,97 @@ +# 🤗 Chat UI + +Open source chat interface with support for tools, web search, multimodal and many API providers. The app uses MongoDB and SvelteKit behind the scenes. Try the live version of the app called [HuggingChat on hf.co/chat](https://huggingface.co/chat) or [setup your own instance](./installation/spaces). + +🔧 **[Tools](./configuration/models/tools)**: Function calling with custom tools and support for [Zero GPU spaces](https://huggingface.co/spaces/enzostvs/zero-gpu-spaces) + +🔍 **[Web Search](./configuration/web-search)**: Automated web search, scraping and RAG for all models + +🐙 **[Multimodal](./configuration/models/multimodal)**: Accepts image file uploads on supported providers + +👤 **[OpenID](./configuration/open-id)**: Optionally setup OpenID for user authentication + +
+ +
+Tools +
+ + +
+
+ +
+Web Search +
+ + +
+
+ +
+ +## Quickstart + +You can quickly have a locally running chat-ui & LLM text-generation server thanks to chat-ui's [llama.cpp server support](https://huggingface.co/docs/chat-ui/configuration/models/providers/llamacpp). + +**Step 1 (Start llama.cpp server):** + +```bash +# install llama.cpp +brew install llama.cpp +# start llama.cpp server (using hf.co/microsoft/Phi-3-mini-4k-instruct-gguf as an example) +llama-server --hf-repo microsoft/Phi-3-mini-4k-instruct-gguf --hf-file Phi-3-mini-4k-instruct-q4.gguf -c 4096 +``` + +A local LLaMA.cpp HTTP Server will start on `http://localhost:8080`. Read more [here](https://huggingface.co/docs/chat-ui/configuration/models/providers/llamacpp). + +**Step 2 (tell chat-ui to use local llama.cpp server):** + +Add the following to your `.env.local`: + +```ini +MODELS=`[ + { + "name": "Local microsoft/Phi-3-mini-4k-instruct-gguf", + "tokenizer": "microsoft/Phi-3-mini-4k-instruct-gguf", + "preprompt": "", + "chatPromptTemplate": "{{preprompt}}{{#each messages}}{{#ifUser}}<|user|>\n{{content}}<|end|>\n<|assistant|>\n{{/ifUser}}{{#ifAssistant}}{{content}}<|end|>\n{{/ifAssistant}}{{/each}}", + "parameters": { + "stop": ["<|end|>", "<|endoftext|>", "<|assistant|>"], + "temperature": 0.7, + "max_new_tokens": 1024, + "truncate": 3071 + }, + "endpoints": [{ + "type" : "llamacpp", + "baseURL": "http://localhost:8080" + }], + }, +]` +``` + +Read more [here](https://huggingface.co/docs/chat-ui/configuration/models/providers/llamacpp). + +**Step 3 (make sure you have MongoDb running locally):** + +```bash +docker run -d -p 27017:27017 --name mongo-chatui mongo:latest +``` + +Read more [here](https://github.com/huggingface/chat-ui?tab=Readme-ov-file#database). + +**Step 4 (start chat-ui):** + +```bash +git clone https://github.com/huggingface/chat-ui +cd chat-ui +npm install +npm run dev -- --open +``` + +Read more [here](https://github.com/huggingface/chat-ui?tab=readme-ov-file#launch). + +
+ + +
diff --git a/docs/source/installation/docker.md b/docs/source/installation/docker.md new file mode 100644 index 0000000000000000000000000000000000000000..5b7e3b70884a2b5a6496cf0f8ba582dc7dbbf27b --- /dev/null +++ b/docs/source/installation/docker.md @@ -0,0 +1,11 @@ +# Running on Docker + +Pre-built docker images are provided with and without MongoDB built in. Refer to the [configuration section](../configuration/overview) for env variables that must be provided. We recommend using the `--env-file` option to avoid leaking secrets into your shell history. + +```bash +# Without built-in DB +docker run -p 3000:3000 --env-file .env.local --name chat-ui ghcr.io/huggingface/chat-ui + +# With built-in DB +docker run -p 3000:3000 --env-file .env.local -v chat-ui:/data --name chat-ui ghcr.io/huggingface/chat-ui-db +``` diff --git a/docs/source/installation/helm.md b/docs/source/installation/helm.md new file mode 100644 index 0000000000000000000000000000000000000000..789e3695f515d98182ca18e2a58f5996acd70b38 --- /dev/null +++ b/docs/source/installation/helm.md @@ -0,0 +1,35 @@ +# Helm + + + +**We highly discourage using the chart**. The Helm chart is a work in progress and should be considered unstable. Breaking changes to the chart may be pushed without migration guides or notice. Contributions welcome! + + + +For installation on Kubernetes, you may use the helm chart in `/chart`. Please note that no chart repository has been setup, so you'll need to clone the repository and install the chart by path. The production values may be found at `chart/env/prod.yaml`. + +**Example values.yaml** + +```yaml +replicas: 1 + +domain: example.com + +service: + type: ClusterIP + +resources: + requests: + cpu: 100m + memory: 2Gi + limits: + # Recommended to use large limits when web search is enabled + cpu: "4" + memory: 6Gi + +envVars: + MONGODB_URL: mongodb://chat-ui-mongo:27017 + # Ensure that your values.yaml will not leak anywhere + # PRs welcome for a chart rework with envFrom support! + HF_TOKEN: secret_token +``` diff --git a/docs/source/installation/local.md b/docs/source/installation/local.md new file mode 100644 index 0000000000000000000000000000000000000000..f176d349bbb75522dbc1e14b354239882e31d859 --- /dev/null +++ b/docs/source/installation/local.md @@ -0,0 +1,34 @@ +# Running Locally + +You may start an instance locally for non-production use cases. For production use cases, please see the other installation options. + +## Configuration + +The default config for Chat UI is stored in the `.env` file. You will need to override some values to get Chat UI to run locally. Start by creating a `.env.local` file in the root of the repository as per the [configuration section](../configuration/overview). The bare minimum config you need to get Chat UI to run locally is the following: + +```ini +MONGODB_URL= +HF_TOKEN= # find your token at hf.co/settings/token +``` + +## Database + +The chat history is stored in a MongoDB instance, and having a DB instance available is needed for Chat UI to work. + +You can use a local MongoDB instance. The easiest way is to spin one up using docker with persistence: + +```bash +docker run -d -p 27017:27017 -v mongo-chat-ui:/data --name mongo-chat-ui mongo:latest +``` + +In which case the url of your DB will be `MONGODB_URL=mongodb://localhost:27017`. + +Alternatively, you can use a [free MongoDB Atlas](https://www.mongodb.com/pricing) instance for this, Chat UI should fit comfortably within their free tier. After which you can set the `MONGODB_URL` variable in `.env.local` to match your instance. + +## Starting the server + +```bash +npm ci # install dependencies +npm run build # build the project +npm run preview -- --open # start the server with & open your instance at http://localhost:4173 +``` diff --git a/docs/source/installation/spaces.md b/docs/source/installation/spaces.md new file mode 100644 index 0000000000000000000000000000000000000000..54341744b7b5b7305a37536dbb09e29871782bc7 --- /dev/null +++ b/docs/source/installation/spaces.md @@ -0,0 +1,9 @@ +# Running on Huggingface Spaces + +If you don't want to configure, setup, and launch your own Chat UI yourself, you can use this option as a fast deploy alternative. + +You can deploy your own customized Chat UI instance with any supported [LLM](https://huggingface.co/models?pipeline_tag=text-generation) of your choice on [Hugging Face Spaces](https://huggingface.co/spaces). To do so, use the chat-ui template [available here](https://huggingface.co/new-space?template=huggingchat/chat-ui-template). + +Set `HF_TOKEN` in [Space secrets](https://huggingface.co/docs/hub/spaces-overview#managing-secrets-and-environment-variables) to deploy a model with gated access or a model in a private repository. It's also compatible with [Inference for PROs](https://huggingface.co/blog/inference-pro) curated list of powerful models with higher rate limits. Make sure to create your personal token first in your [User Access Tokens settings](https://huggingface.co/settings/tokens). + +Read the full tutorial [here](https://huggingface.co/docs/hub/spaces-sdks-docker-chatui#chatui-on-spaces). diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000000000000000000000000000000000000..3fc46ee53aa072971739b5a251011b6327f53c32 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,19 @@ +ENV_LOCAL_PATH=/app/.env.local + +if test -z "${DOTENV_LOCAL}" ; then + if ! test -f "${ENV_LOCAL_PATH}" ; then + echo "DOTENV_LOCAL was not found in the ENV variables and .env.local is not set using a bind volume. Make sure to set environment variables properly. " + fi; +else + echo "DOTENV_LOCAL was found in the ENV variables. Creating .env.local file." + cat <<< "$DOTENV_LOCAL" > ${ENV_LOCAL_PATH} +fi; + +if [ "$INCLUDE_DB" = "true" ] ; then + echo "Starting local MongoDB instance" + nohup mongod & +fi; + +export PUBLIC_VERSION=$(node -p "require('./package.json').version") + +dotenv -e /app/.env -c -- node /app/build/index.js -- --host 0.0.0.0 --port 3000 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..156d6565666ff96b05479145923be0390762c812 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,14391 @@ +{ + "name": "chat-ui", + "version": "0.9.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chat-ui", + "version": "0.9.4", + "dependencies": { + "@aws-sdk/credential-providers": "^3.592.0", + "@cliqz/adblocker-playwright": "^1.27.2", + "@gradio/client": "^1.8.0", + "@huggingface/hub": "^0.5.1", + "@huggingface/inference": "^2.8.1", + "@huggingface/transformers": "^3.1.1", + "@iconify-json/bi": "^1.1.21", + "@playwright/browser-chromium": "^1.43.1", + "@resvg/resvg-js": "^2.6.2", + "autoprefixer": "^10.4.14", + "aws-sigv4-fetch": "^4.0.1", + "aws4": "^1.13.0", + "date-fns": "^2.29.3", + "dotenv": "^16.0.3", + "express": "^4.21.2", + "file-type": "^19.4.1", + "google-auth-library": "^9.13.0", + "handlebars": "^4.7.8", + "highlight.js": "^11.7.0", + "husky": "^9.0.11", + "image-size": "^1.0.2", + "ip-address": "^9.0.5", + "jose": "^5.3.0", + "jsdom": "^22.0.0", + "json5": "^2.2.3", + "jsonpath": "^1.1.1", + "katex": "^0.16.21", + "lint-staged": "^15.2.7", + "marked": "^12.0.1", + "mongodb": "^6.14.2", + "nanoid": "^5.0.9", + "openid-client": "^5.4.2", + "parquetjs": "^0.11.2", + "pino": "^9.0.0", + "pino-pretty": "^11.0.0", + "playwright": "^1.44.1", + "postcss": "^8.4.31", + "saslprep": "^1.0.3", + "satori": "^0.10.11", + "satori-html": "^0.3.2", + "sbd": "^1.0.19", + "serpapi": "^1.1.1", + "sharp": "^0.33.4", + "tailwind-scrollbar": "^3.0.0", + "tailwindcss": "^3.4.0", + "uuid": "^10.0.0", + "zod": "^3.22.3" + }, + "devDependencies": { + "@faker-js/faker": "^8.4.1", + "@iconify-json/carbon": "^1.1.16", + "@iconify-json/eos-icons": "^1.1.6", + "@sveltejs/adapter-node": "^5.2.0", + "@sveltejs/kit": "^2.17.1", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/typography": "^0.5.9", + "@types/dompurify": "^3.0.5", + "@types/express": "^4.17.21", + "@types/js-yaml": "^4.0.9", + "@types/jsdom": "^21.1.1", + "@types/jsonpath": "^0.2.4", + "@types/katex": "^0.16.7", + "@types/mime-types": "^2.1.4", + "@types/minimist": "^1.2.5", + "@types/node": "^22.1.0", + "@types/parquetjs": "^0.10.3", + "@types/sbd": "^1.0.5", + "@types/uuid": "^9.0.8", + "@typescript-eslint/eslint-plugin": "^6.x", + "@typescript-eslint/parser": "^6.x", + "dompurify": "^3.2.4", + "eslint": "^8.28.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-svelte": "^2.45.1", + "isomorphic-dompurify": "^2.13.0", + "js-yaml": "^4.1.0", + "minimist": "^1.2.8", + "mongodb-memory-server": "^10.1.2", + "prettier": "^3.1.0", + "prettier-plugin-svelte": "^3.2.6", + "prettier-plugin-tailwindcss": "^0.6.11", + "prom-client": "^15.1.2", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "svelte-gestures": "^5.1.3", + "ts-node": "^10.9.1", + "tslib": "^2.4.1", + "typescript": "^5.5.0", + "unplugin-icons": "^0.16.1", + "vite": "^6.2.3", + "vite-node": "^3.0.9", + "vitest": "^3.0.9" + }, + "optionalDependencies": { + "@anthropic-ai/sdk": "^0.32.1", + "@anthropic-ai/vertex-sdk": "^0.4.1", + "@aws-sdk/client-bedrock-runtime": "^3.631.0", + "@google-cloud/vertexai": "^1.1.0", + "@google/generative-ai": "^0.14.1", + "aws4fetch": "^1.0.17", + "cohere-ai": "^7.9.0", + "openai": "^4.44.0" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.1.tgz", + "integrity": "sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "find-up": "^5.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/install-pkg/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "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/@antfu/install-pkg/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@antfu/install-pkg/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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@antfu/install-pkg/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@antfu/install-pkg/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@antfu/install-pkg/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@antfu/install-pkg/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@antfu/install-pkg/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@antfu/install-pkg/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz", + "integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.85", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.85.tgz", + "integrity": "sha512-61sLB7tXUMpkHJagZQAzPV4xGyqzulLvphe0lquRX80rZG24VupRv9p6Qo06V9VBNeGBM8Sv8rRVVLji6pi7QQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT", + "optional": true + }, + "node_modules/@anthropic-ai/vertex-sdk": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.4.3.tgz", + "integrity": "sha512-2Uef0C5P2Hx+T88RnUSRA3u4aZqmqnrRSOb2N64ozgKPiSUPTM5JlggAq2b32yWMj5d3MLYa6spJXKMmHXOcoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@anthropic-ai/sdk": ">=0.14 <1", + "google-auth-library": "^9.4.2" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.1.tgz", + "integrity": "sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.2", + "@csstools/css-color-parser": "^3.0.8", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/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==", + "dev": true, + "license": "ISC" + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.779.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.779.0.tgz", + "integrity": "sha512-MyzZks8XxWwdsA4VlyPW4IekUjpgDI91VwMvEtOITHD8w+9nTGJtD32HcCKNQQCHWYfSoOj7yIoHRisVatR8yw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.775.0", + "@aws-sdk/credential-provider-node": "3.777.0", + "@aws-sdk/middleware-host-header": "3.775.0", + "@aws-sdk/middleware-logger": "3.775.0", + "@aws-sdk/middleware-recursion-detection": "3.775.0", + "@aws-sdk/middleware-user-agent": "3.775.0", + "@aws-sdk/region-config-resolver": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@aws-sdk/util-endpoints": "3.775.0", + "@aws-sdk/util-user-agent-browser": "3.775.0", + "@aws-sdk/util-user-agent-node": "3.775.0", + "@smithy/config-resolver": "^4.1.0", + "@smithy/core": "^3.2.0", + "@smithy/eventstream-serde-browser": "^4.0.2", + "@smithy/eventstream-serde-config-resolver": "^4.1.0", + "@smithy/eventstream-serde-node": "^4.0.2", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.8", + "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-endpoints": "^3.0.2", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.2", + "@smithy/util-stream": "^4.2.0", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.777.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.777.0.tgz", + "integrity": "sha512-VGtFI3SH+jKfPln+9CM16F9zKieIqSxUSZNzQ6WZahPDVC79VmlG6QkXCqgm9Y4qZf4ebcdMhO23+FkR4s9vhA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.775.0", + "@aws-sdk/credential-provider-node": "3.777.0", + "@aws-sdk/middleware-host-header": "3.775.0", + "@aws-sdk/middleware-logger": "3.775.0", + "@aws-sdk/middleware-recursion-detection": "3.775.0", + "@aws-sdk/middleware-user-agent": "3.775.0", + "@aws-sdk/region-config-resolver": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@aws-sdk/util-endpoints": "3.775.0", + "@aws-sdk/util-user-agent-browser": "3.775.0", + "@aws-sdk/util-user-agent-node": "3.775.0", + "@smithy/config-resolver": "^4.1.0", + "@smithy/core": "^3.2.0", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.8", + "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-endpoints": "^3.0.2", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sagemaker": { + "version": "3.778.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sagemaker/-/client-sagemaker-3.778.0.tgz", + "integrity": "sha512-JOXv7wLPstZXaErBwvajqf/UFiGaqzbBq/qRGwQy+p+LdsT8OKYySXZJTUabifNIyI1Ncx39eub+OEk1YwvPTg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.775.0", + "@aws-sdk/credential-provider-node": "3.777.0", + "@aws-sdk/middleware-host-header": "3.775.0", + "@aws-sdk/middleware-logger": "3.775.0", + "@aws-sdk/middleware-recursion-detection": "3.775.0", + "@aws-sdk/middleware-user-agent": "3.775.0", + "@aws-sdk/region-config-resolver": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@aws-sdk/util-endpoints": "3.775.0", + "@aws-sdk/util-user-agent-browser": "3.775.0", + "@aws-sdk/util-user-agent-node": "3.775.0", + "@smithy/config-resolver": "^4.1.0", + "@smithy/core": "^3.2.0", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.8", + "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-endpoints": "^3.0.2", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.2", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.3", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sagemaker/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.777.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.777.0.tgz", + "integrity": "sha512-0+z6CiAYIQa7s6FJ+dpBYPi9zr9yY5jBg/4/FGcwYbmqWPXwL9Thdtr0FearYRZgKl7bhL3m3dILCCfWqr3teQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.775.0", + "@aws-sdk/middleware-host-header": "3.775.0", + "@aws-sdk/middleware-logger": "3.775.0", + "@aws-sdk/middleware-recursion-detection": "3.775.0", + "@aws-sdk/middleware-user-agent": "3.775.0", + "@aws-sdk/region-config-resolver": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@aws-sdk/util-endpoints": "3.775.0", + "@aws-sdk/util-user-agent-browser": "3.775.0", + "@aws-sdk/util-user-agent-node": "3.775.0", + "@smithy/config-resolver": "^4.1.0", + "@smithy/core": "^3.2.0", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.8", + "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-endpoints": "^3.0.2", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.775.0.tgz", + "integrity": "sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/core": "^3.2.0", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/property-provider": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/signature-v4": "^5.0.2", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/util-middleware": "^4.0.2", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.777.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.777.0.tgz", + "integrity": "sha512-lNvz3v94TvEcBvQqVUyg+c/aL3Max+8wUMXvehWoQPv9y9cJAHciZqvA/G+yFo/JB+1Y4IBpMu09W2lfpT6Euw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.777.0", + "@aws-sdk/types": "3.775.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.775.0.tgz", + "integrity": "sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.775.0.tgz", + "integrity": "sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/property-provider": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/util-stream": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.777.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.777.0.tgz", + "integrity": "sha512-1X9mCuM9JSQPmQ+D2TODt4THy6aJWCNiURkmKmTIPRdno7EIKgAqrr/LLN++K5mBf54DZVKpqcJutXU2jwo01A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.775.0", + "@aws-sdk/credential-provider-env": "3.775.0", + "@aws-sdk/credential-provider-http": "3.775.0", + "@aws-sdk/credential-provider-process": "3.775.0", + "@aws-sdk/credential-provider-sso": "3.777.0", + "@aws-sdk/credential-provider-web-identity": "3.777.0", + "@aws-sdk/nested-clients": "3.777.0", + "@aws-sdk/types": "3.775.0", + "@smithy/credential-provider-imds": "^4.0.2", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.777.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.777.0.tgz", + "integrity": "sha512-ZD66ywx1Q0KyUSuBXZIQzBe3Q7MzX8lNwsrCU43H3Fww+Y+HB3Ncws9grhSdNhKQNeGmZ+MgKybuZYaaeLwJEQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.775.0", + "@aws-sdk/credential-provider-http": "3.775.0", + "@aws-sdk/credential-provider-ini": "3.777.0", + "@aws-sdk/credential-provider-process": "3.775.0", + "@aws-sdk/credential-provider-sso": "3.777.0", + "@aws-sdk/credential-provider-web-identity": "3.777.0", + "@aws-sdk/types": "3.775.0", + "@smithy/credential-provider-imds": "^4.0.2", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.775.0.tgz", + "integrity": "sha512-A6k68H9rQp+2+7P7SGO90Csw6nrUEm0Qfjpn9Etc4EboZhhCLs9b66umUsTsSBHus4FDIe5JQxfCUyt1wgNogg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.777.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.777.0.tgz", + "integrity": "sha512-9mPz7vk9uE4PBVprfINv4tlTkyq1OonNevx2DiXC1LY4mCUCNN3RdBwAY0BTLzj0uyc3k5KxFFNbn3/8ZDQP7w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.777.0", + "@aws-sdk/core": "3.775.0", + "@aws-sdk/token-providers": "3.777.0", + "@aws-sdk/types": "3.775.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.777.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.777.0.tgz", + "integrity": "sha512-uGCqr47fnthkqwq5luNl2dksgcpHHjSXz2jUra7TXtFOpqvnhOW8qXjoa1ivlkq8qhqlaZwCzPdbcN0lXpmLzQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.775.0", + "@aws-sdk/nested-clients": "3.777.0", + "@aws-sdk/types": "3.775.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.778.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.778.0.tgz", + "integrity": "sha512-Yy1RSBvoDp/iqGDpmgy5/YnSP2ac9NxTv3wdAjKlqVVStlKWU9nG8MPHZRfy01oPNJ5YWZL9stxHjNKC9hg9eg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.777.0", + "@aws-sdk/core": "3.775.0", + "@aws-sdk/credential-provider-cognito-identity": "3.777.0", + "@aws-sdk/credential-provider-env": "3.775.0", + "@aws-sdk/credential-provider-http": "3.775.0", + "@aws-sdk/credential-provider-ini": "3.777.0", + "@aws-sdk/credential-provider-node": "3.777.0", + "@aws-sdk/credential-provider-process": "3.775.0", + "@aws-sdk/credential-provider-sso": "3.777.0", + "@aws-sdk/credential-provider-web-identity": "3.777.0", + "@aws-sdk/nested-clients": "3.777.0", + "@aws-sdk/types": "3.775.0", + "@smithy/config-resolver": "^4.1.0", + "@smithy/core": "^3.2.0", + "@smithy/credential-provider-imds": "^4.0.2", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.775.0.tgz", + "integrity": "sha512-tkSegM0Z6WMXpLB8oPys/d+umYIocvO298mGvcMCncpRl77L9XkvSLJIFzaHes+o7djAgIduYw8wKIMStFss2w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.775.0.tgz", + "integrity": "sha512-FaxO1xom4MAoUJsldmR92nT1G6uZxTdNYOFYtdHfd6N2wcNaTuxgjIvqzg5y7QIH9kn58XX/dzf1iTjgqUStZw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.775.0.tgz", + "integrity": "sha512-GLCzC8D0A0YDG5u3F5U03Vb9j5tcOEFhr8oc6PDk0k0vm5VwtZOE6LvK7hcCSoAB4HXyOUM0sQuXrbaAh9OwXA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.775.0.tgz", + "integrity": "sha512-7Lffpr1ptOEDE1ZYH1T78pheEY1YmeXWBfFt/amZ6AGsKSLG+JPXvof3ltporTGR2bhH/eJPo7UHCglIuXfzYg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@aws-sdk/util-endpoints": "3.775.0", + "@smithy/core": "^3.2.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.777.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.777.0.tgz", + "integrity": "sha512-bmmVRsCjuYlStYPt06hr+f8iEyWg7+AklKCA8ZLDEJujXhXIowgUIqXmqpTkXwkVvDQ9tzU7hxaONjyaQCGybA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.775.0", + "@aws-sdk/middleware-host-header": "3.775.0", + "@aws-sdk/middleware-logger": "3.775.0", + "@aws-sdk/middleware-recursion-detection": "3.775.0", + "@aws-sdk/middleware-user-agent": "3.775.0", + "@aws-sdk/region-config-resolver": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@aws-sdk/util-endpoints": "3.775.0", + "@aws-sdk/util-user-agent-browser": "3.775.0", + "@aws-sdk/util-user-agent-node": "3.775.0", + "@smithy/config-resolver": "^4.1.0", + "@smithy/core": "^3.2.0", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.8", + "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-endpoints": "^3.0.2", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/protocol-http": { + "version": "3.374.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.374.0.tgz", + "integrity": "sha512-9WpRUbINdGroV3HiZZIBoJvL2ndoWk39OfwxWs2otxByppJZNN14bg/lvCx5e8ggHUti7IBk5rb0nqQZ4m05pg==", + "deprecated": "This package has moved to @smithy/protocol-http", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/protocol-http": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/protocol-http/node_modules/@smithy/protocol-http": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-1.2.0.tgz", + "integrity": "sha512-GfGfruksi3nXdFok5RhgtOnWe5f6BndzYfmEXISD+5gAGdayFGpjWu5pIqIweTudMtse20bGbc+7MFZXT1Tb8Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/protocol-http/node_modules/@smithy/types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-1.2.0.tgz", + "integrity": "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.775.0.tgz", + "integrity": "sha512-40iH3LJjrQS3LKUJAl7Wj0bln7RFPEvUYKFxtP8a+oKFDO0F65F52xZxIJbPn6sHkxWDAnZlGgdjZXM3p2g5wQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4": { + "version": "3.374.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.374.0.tgz", + "integrity": "sha512-2xLJvSdzcZZAg0lsDLUAuSQuihzK0dcxIK7WmfuJeF7DGKJFmp9czQmz5f3qiDz6IDQzvgK1M9vtJSVCslJbyQ==", + "deprecated": "This package has moved to @smithy/signature-v4", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/signature-v4": "^1.0.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@aws-crypto/crc32": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", + "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@aws-crypto/crc32/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", + "optional": true + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@aws-crypto/util/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", + "optional": true + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@smithy/eventstream-codec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-1.1.0.tgz", + "integrity": "sha512-3tEbUb8t8an226jKB6V/Q2XU/J53lCwCzULuBPEaF4JjSh+FlCMp7TmogE/Aij5J9DwlsZ4VAD/IRDuQ/0ZtMw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^1.2.0", + "@smithy/util-hex-encoding": "^1.1.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@smithy/is-array-buffer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-1.1.0.tgz", + "integrity": "sha512-twpQ/n+3OWZJ7Z+xu43MJErmhB/WO/mMTnqR6PwWQShvSJ/emx5d1N59LQZk6ZpTAeuRWrc+eHhkzTp9NFjNRQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@smithy/signature-v4": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-1.1.0.tgz", + "integrity": "sha512-fDo3m7YqXBs7neciOePPd/X9LPm5QLlDMdIC4m1H6dgNLnXfLMFNIxEfPyohGA8VW9Wn4X8lygnPSGxDZSmp0Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/eventstream-codec": "^1.1.0", + "@smithy/is-array-buffer": "^1.1.0", + "@smithy/types": "^1.2.0", + "@smithy/util-hex-encoding": "^1.1.0", + "@smithy/util-middleware": "^1.1.0", + "@smithy/util-uri-escape": "^1.1.0", + "@smithy/util-utf8": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@smithy/types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-1.2.0.tgz", + "integrity": "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@smithy/util-buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-1.1.0.tgz", + "integrity": "sha512-9m6NXE0ww+ra5HKHCHig20T+FAwxBAm7DIdwc/767uGWbRcY720ybgPacQNB96JMOI7xVr/CDa3oMzKmW4a+kw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/is-array-buffer": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@smithy/util-hex-encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-1.1.0.tgz", + "integrity": "sha512-7UtIE9eH0u41zpB60Jzr0oNCQ3hMJUabMcKRUVjmyHTXiWDE4vjSqN6qlih7rCNeKGbioS7f/y2Jgym4QZcKFg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@smithy/util-middleware": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-1.1.0.tgz", + "integrity": "sha512-6hhckcBqVgjWAqLy2vqlPZ3rfxLDhFWEmM7oLh2POGvsi7j0tHkbN7w4DFhuBExVJAbJ/qqxqZdRY6Fu7/OezQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@smithy/util-uri-escape": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-1.1.0.tgz", + "integrity": "sha512-/jL/V1xdVRt5XppwiaEU8Etp5WHZj609n0xMTuehmCqdoOFbId1M+aEeDWZsQ+8JbEB/BJ6ynY2SlYmOaKtt8w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4/node_modules/@smithy/util-utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-1.1.0.tgz", + "integrity": "sha512-p/MYV+JmqmPyjdgyN2UxAeYDj9cBqCjp0C/NsTWnnjoZUVqoeZ6IrW915L9CAKWVECgv9lVQGc4u/yz26/bI1A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/util-buffer-from": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.777.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.777.0.tgz", + "integrity": "sha512-Yc2cDONsHOa4dTSGOev6Ng2QgTKQUEjaUnsyKd13pc/nLLz/WLqHiQ/o7PcnKERJxXGs1g1C6l3sNXiX+kbnFQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.777.0", + "@aws-sdk/types": "3.775.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.775.0.tgz", + "integrity": "sha512-ZoGKwa4C9fC9Av6bdfqcW6Ix5ot05F/S4VxWR2nHuMv7hzfmAjTOcUiWT7UR4hM/U0whf84VhDtXN/DWAk52KA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.775.0.tgz", + "integrity": "sha512-yjWmUgZC9tUxAo8Uaplqmq0eUh0zrbZJdwxGRKdYxfm4RG6fMw1tj52+KkatH7o+mNZvg1GDcVp/INktxonJLw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/types": "^4.2.0", + "@smithy/util-endpoints": "^3.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.723.0.tgz", + "integrity": "sha512-Yf2CS10BqK688DRsrKI/EO6B8ff5J86NXe4C+VCysK7UOgN0l1zOTeTukZ3H8Q9tYYX3oaF1961o8vRkFm7Nmw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.775.0.tgz", + "integrity": "sha512-txw2wkiJmZKVdDbscK7VBK+u+TJnRtlUjRTLei+elZg2ADhpQxfVAQl436FUeIv6AhB/oRHW6/K/EAGXUSWi0A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/types": "^4.2.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.775.0.tgz", + "integrity": "sha512-N9yhTevbizTOMo3drH7Eoy6OkJ3iVPxhV7dwb6CMAObbLneS36CSfA6xQXupmHWcRvZPTz8rd1JGG3HzFOau+g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "license": "ISC", + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/cookie/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "license": "ISC", + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "license": "ISC", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@cliqz/adblocker": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/@cliqz/adblocker/-/adblocker-1.34.0.tgz", + "integrity": "sha512-d7TeUl5t+TOMJe7/CRYtf+x6hbd8N25DtH7guQTIjjr3AFVortxiAIgNejGvVqy0by4eNByw+oVil15oqxz2Eg==", + "deprecated": "This project has been renamed to @ghostery/adblocker. Install using @ghostery/adblocker instead", + "license": "MPL-2.0", + "dependencies": { + "@cliqz/adblocker-content": "^1.34.0", + "@cliqz/adblocker-extended-selectors": "^1.34.0", + "@remusao/guess-url-type": "^1.3.0", + "@remusao/small": "^1.2.1", + "@remusao/smaz": "^1.9.1", + "@types/chrome": "^0.0.278", + "@types/firefox-webext-browser": "^120.0.0", + "tldts-experimental": "^6.0.14" + } + }, + "node_modules/@cliqz/adblocker-content": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/@cliqz/adblocker-content/-/adblocker-content-1.34.0.tgz", + "integrity": "sha512-5LcV8UZv49RWwtpom9ve4TxJIFKd+bjT59tS/2Z2c22Qxx5CW1ncO/T+ybzk31z422XplQfd0ZE6gMGGKs3EMg==", + "deprecated": "This project has been renamed to @ghostery/adblocker-content. Install using @ghostery/adblocker-content instead", + "license": "MPL-2.0", + "dependencies": { + "@cliqz/adblocker-extended-selectors": "^1.34.0" + } + }, + "node_modules/@cliqz/adblocker-extended-selectors": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/@cliqz/adblocker-extended-selectors/-/adblocker-extended-selectors-1.34.0.tgz", + "integrity": "sha512-lNrgdUPpsBWHjrwXy2+Z5nX/Gy5YAvNwFMLqkeMdjzrybwPIalJJN2e+YtkS1I6mVmOMNppF5cv692OAVoI74g==", + "deprecated": "This project has been renamed to @ghostery/adblocker-extended-selectors. Install using @ghostery/adblocker-extended-selectors instead", + "license": "MPL-2.0" + }, + "node_modules/@cliqz/adblocker-playwright": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/@cliqz/adblocker-playwright/-/adblocker-playwright-1.34.0.tgz", + "integrity": "sha512-YMedgiz9LR5VW6ocKoC1P3cSsj1T9Ibinp14beXxvpydMmneX+fQB0Hq4bqWvuuL3CNl7fENMgiCDDMTgMLqww==", + "deprecated": "This project has been renamed to @ghostery/adblocker-playwright. Install using @ghostery/adblocker-playwright instead", + "license": "MPL-2.0", + "dependencies": { + "@cliqz/adblocker": "^1.34.0", + "@cliqz/adblocker-content": "^1.34.0", + "tldts-experimental": "^6.0.14" + }, + "peerDependencies": { + "playwright": "^1.x" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz", + "integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz", + "integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.0.tgz", + "integrity": "sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/vertexai": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-1.9.3.tgz", + "integrity": "sha512-35o5tIEMLW3JeFJOaaMNR2e5sq+6rpnhrF97PuAxeOm0GlqVTESKhkGj7a5B5mmJSSSU3hUfIhcQCRRsw4Ipzg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "google-auth-library": "^9.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@google/generative-ai": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.14.1.tgz", + "integrity": "sha512-pevEyZCb0Oc+dYNlSberW8oZBm4ofeTD5wN01TowQMhTwdAbGAnJMtQzoklh6Blq2AKsx8Ox6FWa44KioZLZiA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@gradio/client": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@gradio/client/-/client-1.14.0.tgz", + "integrity": "sha512-BqM4D6RNCjInJG0OcGrAbik+DX4kRht2XrB7Mkd6bT5rtP3pUStZFRqb+mOuw+wvZvDfm4K0SngB/9X2i+RtTg==", + "license": "ISC", + "dependencies": { + "@types/eventsource": "^1.1.15", + "bufferutil": "^4.0.7", + "eventsource": "^2.0.2", + "fetch-event-stream": "^0.1.5", + "msw": "^2.2.1", + "semiver": "^1.1.0", + "textlinestream": "^1.1.1", + "typescript": "^5.0.0", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@huggingface/hub": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-0.5.1.tgz", + "integrity": "sha512-ZaE2gY8NY+XwIOL7+gBhPq19PXG4gbGSSJ7zwWLoq6MKP+nsgkQk/c7fBFrxgBwR6lNd0AJMHPRCjwTndqsqWQ==", + "license": "MIT", + "dependencies": { + "hash-wasm": "^4.9.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/inference": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-2.8.1.tgz", + "integrity": "sha512-EfsNtY9OR6JCNaUa5bZu2mrs48iqeTz0Gutwf+fU0Kypx33xFQB4DKMhp8u4Ee6qVbLbNWvTHuWwlppLQl4p4Q==", + "license": "MIT", + "dependencies": { + "@huggingface/tasks": "^0.12.9" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/jinja": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.3.3.tgz", + "integrity": "sha512-vQQr2JyWvVFba3Lj9es4q9vCl1sAc74fdgnEMoX8qHrXtswap9ge9uO3ONDzQB0cQ0PUyaKY2N6HaVbTBvSXvw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/tasks": { + "version": "0.12.30", + "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.12.30.tgz", + "integrity": "sha512-A1ITdxbEzx9L8wKR8pF7swyrTLxWNDFIGDLUWInxvks2ruQ8PLRBZe8r0EcjC3CDdtlj9jV1V4cgV35K/iy3GQ==", + "license": "MIT" + }, + "node_modules/@huggingface/transformers": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.4.1.tgz", + "integrity": "sha512-Inbvq9i/33kmd5XHom9MQU7NAOV5UcGmHBwBk9NFw4IPhdoTnfP7wFJxJmceYhRdS+EL1Hpw4he/Ceimau6ORg==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.3.3", + "onnxruntime-node": "1.20.1", + "onnxruntime-web": "1.22.0-dev.20250306-ccf8fdd9ea", + "sharp": "^0.33.5" + } + }, + "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", + "dev": true, + "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/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "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", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@iconify-json/bi": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@iconify-json/bi/-/bi-1.2.2.tgz", + "integrity": "sha512-f/Wm+RTdBosw3cI/gmg5uSFmdumkw2thUk4qVFS56jTerdal6wqWnWAbRTSJJ/vhH9/y16pRKnvE8F2+M23dzw==", + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/carbon": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@iconify-json/carbon/-/carbon-1.2.8.tgz", + "integrity": "sha512-6xh4YiFBz6qoSnB3XMe23WvjTJroDFXB17J1MbiT7nATFe+70+em1acRXr8hgP/gYpwFMHFc4IvjA/IPTPnTzg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/eos-icons": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@iconify-json/eos-icons/-/eos-icons-1.2.2.tgz", + "integrity": "sha512-kYfV1WfgiHDbWdG9JEbV1K77MvksRmo9KIM4VjtYFMnF8pKqTv4MoLIOdCD2lbRlhBZspxr1GKde5Z/LYqz3Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@antfu/utils": "^8.1.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.0", + "globals": "^15.14.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "mlly": "^1.7.4" + } + }, + "node_modules/@iconify/utils/node_modules/@antfu/install-pkg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz", + "integrity": "sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^0.2.8", + "tinyexec": "^0.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@iconify/utils/node_modules/@antfu/utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", + "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@iconify/utils/node_modules/confbox": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.1.tgz", + "integrity": "sha512-hkT3yDPFbs95mNCy1+7qNKC6Pro+/ibzYxtM2iqEigpf0sVw+bg4Zh9/snjsBcf990vfIsg5+1U7VyiyBb3etg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@iconify/utils/node_modules/local-pkg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz", + "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.0.1", + "quansync": "^0.2.8" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@iconify/utils/node_modules/pkg-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", + "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.1", + "exsolve": "^1.0.1", + "pathe": "^2.0.3" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.8.tgz", + "integrity": "sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.9.tgz", + "integrity": "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", + "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", + "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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/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/@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/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.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/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/@mongodb-js/saslprep": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", + "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.37.6", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", + "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==", + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "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/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "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/@playwright/browser-chromium": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.51.1.tgz", + "integrity": "sha512-Xebxk0SrDKttd8VGiUwLxOMbuH/Lf/+vFyzFG7QHVvqsAOw3Ec7Xdl1HRB4dnVP/RTEytkH4OgQ4OFy6K2c1xw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.51.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@remusao/guess-url-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@remusao/guess-url-type/-/guess-url-type-1.3.0.tgz", + "integrity": "sha512-SNSJGxH5ckvxb3EUHj4DqlAm/bxNxNv2kx/AESZva/9VfcBokwKNS+C4D1lQdWIDM1R3d3UG+xmVzlkNG8CPTQ==", + "license": "MPL-2.0" + }, + "node_modules/@remusao/small": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@remusao/small/-/small-1.3.0.tgz", + "integrity": "sha512-bydAhJI+ywmg5xMUcbqoR8KahetcfkFywEZpsyFZ8EBofilvWxbXnMSe4vnjDI1Y+SWxnNhR4AL/2BAXkf4b8A==", + "license": "MPL-2.0" + }, + "node_modules/@remusao/smaz": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@remusao/smaz/-/smaz-1.10.0.tgz", + "integrity": "sha512-GQzCxmmMpLkyZwcwNgz8TpuBEWl0RUQa8IcvKiYlPxuyYKqyqPkCr0hlHI15ckn3kDUPS68VmTVgyPnLNrdVmg==", + "license": "MPL-2.0", + "dependencies": { + "@remusao/smaz-compress": "^1.10.0", + "@remusao/smaz-decompress": "^1.10.0" + } + }, + "node_modules/@remusao/smaz-compress": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@remusao/smaz-compress/-/smaz-compress-1.10.0.tgz", + "integrity": "sha512-E/lC8OSU+3bQrUl64vlLyPzIxo7dxF2RvNBe9KzcM4ax43J/d+YMinmMztHyCIHqRbz7rBCtkp3c0KfeIbHmEg==", + "license": "MPL-2.0", + "dependencies": { + "@remusao/trie": "^1.5.0" + } + }, + "node_modules/@remusao/smaz-decompress": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@remusao/smaz-decompress/-/smaz-decompress-1.10.0.tgz", + "integrity": "sha512-aA5ImUH480Pcs5/cOgToKmFnzi7osSNG6ft+7DdmQTaQEEst3nLq3JLlBEk+gwidURymjbx6DYs60LHaZ415VQ==", + "license": "MPL-2.0" + }, + "node_modules/@remusao/trie": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@remusao/trie/-/trie-1.5.0.tgz", + "integrity": "sha512-UX+3utJKgwCsg6sUozjxd38gNMVRXrY4TNX9VvCdSrlZBS1nZjRPi98ON3QjRAdf6KCguJFyQARRsulTeqQiPg==", + "license": "MPL-2.0" + }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.6.2", + "@resvg/resvg-js-android-arm64": "2.6.2", + "@resvg/resvg-js-darwin-arm64": "2.6.2", + "@resvg/resvg-js-darwin-x64": "2.6.2", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", + "@resvg/resvg-js-linux-x64-musl": "2.6.2", + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", + "cpu": [ + "ia32" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz", + "integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", + "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.38.0.tgz", + "integrity": "sha512-ldomqc4/jDZu/xpYU+aRxo3V4mGCV9HeTgUBANI3oIQMOL+SsxB+S2lxMpkFp5UamSS3XuTMQVbsS24R4J4Qjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.38.0.tgz", + "integrity": "sha512-VUsgcy4GhhT7rokwzYQP+aV9XnSLkkhlEJ0St8pbasuWO/vwphhZQxYEKUP3ayeCYLhk6gEtacRpYP/cj3GjyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.38.0.tgz", + "integrity": "sha512-buA17AYXlW9Rn091sWMq1xGUvWQFOH4N1rqUxGJtEQzhChxWjldGCCup7r/wUnaI6Au8sKXpoh0xg58a7cgcpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.38.0.tgz", + "integrity": "sha512-Mgcmc78AjunP1SKXl624vVBOF2bzwNWFPMP4fpOu05vS0amnLcX8gHIge7q/lDAHy3T2HeR0TqrriZDQS2Woeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.38.0.tgz", + "integrity": "sha512-zzJACgjLbQTsscxWqvrEQAEh28hqhebpRz5q/uUd1T7VTwUNZ4VIXQt5hE7ncs0GrF+s7d3S4on4TiXUY8KoQA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.38.0.tgz", + "integrity": "sha512-hCY/KAeYMCyDpEE4pTETam0XZS4/5GXzlLgpi5f0IaPExw9kuB+PDTOTLuPtM10TlRG0U9OSmXJ+Wq9J39LvAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.38.0.tgz", + "integrity": "sha512-mimPH43mHl4JdOTD7bUMFhBdrg6f9HzMTOEnzRmXbOZqjijCw8LA5z8uL6LCjxSa67H2xiLFvvO67PT05PRKGg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.38.0.tgz", + "integrity": "sha512-tPiJtiOoNuIH8XGG8sWoMMkAMm98PUwlriOFCCbZGc9WCax+GLeVRhmaxjJtz6WxrPKACgrwoZ5ia/uapq3ZVg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.38.0.tgz", + "integrity": "sha512-wZco59rIVuB0tjQS0CSHTTUcEde+pXQWugZVxWaQFdQQ1VYub/sTrNdY76D1MKdN2NB48JDuGABP6o6fqos8mA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.38.0.tgz", + "integrity": "sha512-fQgqwKmW0REM4LomQ+87PP8w8xvU9LZfeLBKybeli+0yHT7VKILINzFEuggvnV9M3x1Ed4gUBmGUzCo/ikmFbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.38.0.tgz", + "integrity": "sha512-hz5oqQLXTB3SbXpfkKHKXLdIp02/w3M+ajp8p4yWOWwQRtHWiEOCKtc9U+YXahrwdk+3qHdFMDWR5k+4dIlddg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.38.0.tgz", + "integrity": "sha512-NXqygK/dTSibQ+0pzxsL3r4Xl8oPqVoWbZV9niqOnIHV/J92fe65pOir0xjkUZDRSPyFRvu+4YOpJF9BZHQImw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.38.0.tgz", + "integrity": "sha512-GEAIabR1uFyvf/jW/5jfu8gjM06/4kZ1W+j1nWTSSB3w6moZEBm7iBtzwQ3a1Pxos2F7Gz+58aVEnZHU295QTg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.38.0.tgz", + "integrity": "sha512-9EYTX+Gus2EGPbfs+fh7l95wVADtSQyYw4DfSBcYdUEAmP2lqSZY0Y17yX/3m5VKGGJ4UmIH5LHLkMJft3bYoA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.38.0.tgz", + "integrity": "sha512-Mpp6+Z5VhB9VDk7RwZXoG2qMdERm3Jw07RNlXHE0bOnEeX+l7Fy4bg+NxfyN15ruuY3/7Vrbpm75J9QHFqj5+Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.38.0.tgz", + "integrity": "sha512-vPvNgFlZRAgO7rwncMeE0+8c4Hmc+qixnp00/Uv3ht2x7KYrJ6ERVd3/R0nUtlE6/hu7/HiiNHJ/rP6knRFt1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.38.0.tgz", + "integrity": "sha512-q5Zv+goWvQUGCaL7fU8NuTw8aydIL/C9abAVGCzRReuj5h30TPx4LumBtAidrVOtXnlB+RZkBtExMsfqkMfb8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.38.0.tgz", + "integrity": "sha512-u/Jbm1BU89Vftqyqbmxdq14nBaQjQX1HhmsdBWqSdGClNaKwhjsg5TpW+5Ibs1mb8Es9wJiMdl86BcmtUVXNZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.38.0.tgz", + "integrity": "sha512-mqu4PzTrlpNHHbu5qleGvXJoGgHpChBlrBx/mEhTPpnAL1ZAYFlvHD7rLK839LLKQzqEQMFJfGrrOHItN4ZQqA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.38.0.tgz", + "integrity": "sha512-jjqy3uWlecfB98Psxb5cD6Fny9Fupv9LrDSPTQZUROqjvZmcCqNu4UMl7qqhlUUGpwiAkotj6GYu4SZdcr/nLw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.2.tgz", + "integrity": "sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.0.tgz", + "integrity": "sha512-8smPlwhga22pwl23fM5ew4T9vfLUCeFXlcqNOCD5M5h8VmNPNUE9j6bQSuRXpDSV11L/E/SwEBQuW8hr6+nS1A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.2.0.tgz", + "integrity": "sha512-k17bgQhVZ7YmUvA8at4af1TDpl0NDMBuBKJl8Yg0nrefwmValU+CnA5l/AriVdQNthU/33H3nK71HrLgqOPr1Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.3", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-stream": "^4.2.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.2.tgz", + "integrity": "sha512-32lVig6jCaWBHnY+OEQ6e6Vnt5vDHaLiydGrwYMW9tPqO688hPGTYRamYJ1EptxEC2rAwJrHWmPoKRBl4iTa8w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.2", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.2.tgz", + "integrity": "sha512-p+f2kLSK7ZrXVfskU/f5dzksKTewZk8pJLPvER3aFHPt76C2MxD9vNatSfLzzQSQB4FNO96RK4PSXfhD1TTeMQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.2.0", + "@smithy/util-hex-encoding": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.2.tgz", + "integrity": "sha512-CepZCDs2xgVUtH7ZZ7oDdZFH8e6Y2zOv8iiX6RhndH69nlojCALSKK+OXwZUgOtUZEUaZ5e1hULVCHYbCn7pug==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.0.tgz", + "integrity": "sha512-1PI+WPZ5TWXrfj3CIoKyUycYynYJgZjuQo8U+sphneOtjsgrttYybdqESFReQrdWJ+LKt6NEdbYzmmfDBmjX2A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.2.tgz", + "integrity": "sha512-C5bJ/C6x9ENPMx2cFOirspnF9ZsBVnBMtP6BdPl/qYSuUawdGQ34Lq0dMcf42QTjUZgWGbUIZnz6+zLxJlb9aw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.2.tgz", + "integrity": "sha512-St8h9JqzvnbB52FtckiHPN4U/cnXcarMniXRXTKn0r4b4XesZOGiAyUdj1aXbqqn1icSqBlzzUsCl6nPB018ng==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/eventstream-codec": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.2.tgz", + "integrity": "sha512-+9Dz8sakS9pe7f2cBocpJXdeVjMopUDLgZs1yWeu7h++WqSbjUYv/JAJwKwXw1HV6gq1jyWjxuyn24E2GhoEcQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.0", + "@smithy/querystring-builder": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.2.tgz", + "integrity": "sha512-VnTpYPnRUE7yVhWozFdlxcYknv9UN7CeOqSrMH+V877v4oqtVYuoqhIhtSjmGPvYrYnAkaM61sLMKHvxL138yg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.2.tgz", + "integrity": "sha512-GatB4+2DTpgWPday+mnUkoumP54u/MDM/5u44KF9hIu8jF0uafZtQLcdfIKkIcUNuF/fBojpLEHZS/56JqPeXQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.2.tgz", + "integrity": "sha512-hAfEXm1zU+ELvucxqQ7I8SszwQ4znWMbNv6PLMndN83JJN41EPuS93AIyh2N+gJ6x8QFhzSO6b7q2e6oClDI8A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.0.tgz", + "integrity": "sha512-xhLimgNCbCzsUppRTGXWkZywksuTThxaIB0HwbpsVLY5sceac4e1TZ/WKYqufQLaUy+gUSJGNdwD2jo3cXL0iA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.2.0", + "@smithy/middleware-serde": "^4.0.3", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-middleware": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.0.tgz", + "integrity": "sha512-2zAagd1s6hAaI/ap6SXi5T3dDwBOczOMCSkkYzktqN1+tzbk1GAsHNAdo/1uzxz3Ky02jvZQwbi/vmDA6z4Oyg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/service-error-classification": "^4.0.2", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.2", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.3.tgz", + "integrity": "sha512-rfgDVrgLEVMmMn0BI8O+8OVr6vXzjV7HZj57l0QxslhzbvVfikZbVfBVthjLHqib4BW44QhcIgJpvebHlRaC9A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.2.tgz", + "integrity": "sha512-eSPVcuJJGVYrFYu2hEq8g8WWdJav3sdrI4o2c6z/rjnYDd3xH9j9E7deZQCzFn4QvGPouLngH3dQ+QVTxv5bOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.0.2.tgz", + "integrity": "sha512-WgCkILRZfJwJ4Da92a6t3ozN/zcvYyJGUTmfGbgS/FkCcoCjl7G4FJaCDN1ySdvLvemnQeo25FdkyMSTSwulsw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.4.tgz", + "integrity": "sha512-/mdqabuAT3o/ihBGjL94PUbTSPSRJ0eeVTdgADzow0wRJ0rN4A27EOrtlK56MYiO1fDvlO3jVTCxQtQmK9dZ1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/querystring-builder": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.2.tgz", + "integrity": "sha512-wNRoQC1uISOuNc2s4hkOYwYllmiyrvVXWMtq+TysNRVQaHm4yoafYQyjN/goYZS+QbYlPIbb/QRjaUZMuzwQ7A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.2.tgz", + "integrity": "sha512-NTOs0FwHw1vimmQM4ebh+wFQvOwkEf/kQL6bSM1Lock+Bv4I89B3hGYoUEPkmvYPkDKyp5UdXJYu+PoTQ3T31Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.2.tgz", + "integrity": "sha512-v6w8wnmZcVXjfVLjxw8qF7OwESD9wnpjp0Dqry/Pod0/5vcEA3qxCr+BhbOHlxS8O+29eLpT3aagxXGwIoEk7Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.2.tgz", + "integrity": "sha512-LA86xeFpTKn270Hbkixqs5n73S+LVM0/VZco8dqd+JT75Dyx3Lcw/MraL7ybjmz786+160K8rPOmhsq0SocoJQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.2.tgz", + "integrity": "sha512-J9/gTWBGVuFZ01oVA6vdb4DAjf1XbDhK6sLsu3OS9qmLrS6KB5ygpeHiM3miIbj1qgSJ96GYszXFWv6ErJ8QEw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.0.2.tgz", + "integrity": "sha512-Mz+mc7okA73Lyz8zQKJNyr7lIcHLiPYp0+oiqiMNc/t7/Kf2BENs5d63pEj7oPqdjaum6g0Fc8wC78dY1TgtXw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.0.tgz", + "integrity": "sha512-Qs65/w30pWV7LSFAez9DKy0Koaoh3iHhpcpCCJ4waj/iqwsuSzJna2+vYwq46yBaqO5ZbP9TjUsATUNxrKeBdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.2.0", + "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "@smithy/util-stream": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.2.0.tgz", + "integrity": "sha512-7eMk09zQKCO+E/ivsjQv+fDlOupcFUCSC/L2YUPgwhvowVGWbPQHjEFcmjt7QQ4ra5lyowS92SV53Zc6XD4+fg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.2.tgz", + "integrity": "sha512-Bm8n3j2ScqnT+kJaClSVCMeiSenK6jVAzZCNewsYWuZtnBehEz4r2qP0riZySZVfzB+03XZHJeqfmJDkeeSLiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.8.tgz", + "integrity": "sha512-ZTypzBra+lI/LfTYZeop9UjoJhhGRTg3pxrNpfSTQLd3AJ37r2z4AXTKpq1rFXiiUIJsYyFgNJdjWRGP/cbBaQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.2", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.8.tgz", + "integrity": "sha512-Rgk0Jc/UDfRTzVthye/k2dDsz5Xxs9LZaKCNPgJTRyoyBoeiNCnHsYGOyu1PKN+sDyPnJzMOz22JbwxzBp9NNA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.0", + "@smithy/credential-provider-imds": "^4.0.2", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/property-provider": "^4.0.2", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.2.tgz", + "integrity": "sha512-6QSutU5ZyrpNbnd51zRTL7goojlcnuOB55+F9VBD+j8JpRY50IGamsjlycrmpn8PQkmJucFW8A0LSfXj7jjtLQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.2.tgz", + "integrity": "sha512-6GDamTGLuBQVAEuQ4yDQ+ti/YINf/MEmIegrEeg7DdB/sld8BX1lqt9RRuIcABOhAGTA50bRbPzErez7SlDtDQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.2.tgz", + "integrity": "sha512-Qryc+QG+7BCpvjloFLQrmlSd0RsVRHejRXd78jNO3+oREueCjwG1CCEH1vduw/ZkM1U9TztwIKVIi3+8MJScGg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.0.tgz", + "integrity": "sha512-Vj1TtwWnuWqdgQI6YTUF5hQ/0jmFiOYsc51CSMgj7QfyO+RF4EnT2HNjoviNlOOmgzgvf3f5yno+EiC4vrnaWQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/types": "^4.2.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.3.tgz", + "integrity": "sha512-JtaY3FxmD+te+KSI2FJuEcfNC9T/DGGVf551babM7fAaXhjJUt7oSYurH1Devxd2+BOSUACCgt3buinx4UnmEA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/abort-controller": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.2.12", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz", + "integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.9.5" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.20.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.2.tgz", + "integrity": "sha512-Dv8TOAZC9vyfcAB9TMsvUEJsRbklRTeNfcYBPaeH6KnABJ99i3CvCB2eNx8fiiliIqe+9GIchBg4RodRH5p1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.2", + "import-meta-resolve": "^4.1.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz", + "integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.0", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.15", + "vitefu": "^1.0.4" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.278", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.278.tgz", + "integrity": "sha512-PDIJodOu7o54PpSOYLybPW/MDZBCjM1TKgf31I3Q/qaEbNpIH09rOM3tSEH3N7Q+FAqb1933LhF8ksUPYeQLNg==", + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eventsource": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz", + "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "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": "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "license": "MIT" + }, + "node_modules/@types/firefox-webext-browser": { + "version": "120.0.4", + "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-120.0.4.tgz", + "integrity": "sha512-lBrpf08xhiZBigrtdQfUaqX1UauwZ+skbFiL8u2Tdra/rklkKadYmIzTwkNZSWtuZ7OKpFqbE2HHfDoFqvZf6w==", + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.16.tgz", + "integrity": "sha512-15tM+qA4Ypml/N7kyRdvfRjBQT2RL461uF1Bldn06K0Nzn1lY3nAPgHlsVrJxdZ9WhZiW0Fmc1lOYMtDsAuB3w==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-int64": { + "version": "0.4.32", + "resolved": "https://registry.npmjs.org/@types/node-int64/-/node-int64-0.4.32.tgz", + "integrity": "sha512-xf/JsSlnXQ+mzvc0IpXemcrO4BrCfpgNpMco+GLcXkFk01k/gW9lGJu+Vof0ZSvHK6DsHJDPSbjFPs36QkWXqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parquetjs": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@types/parquetjs/-/parquetjs-0.10.6.tgz", + "integrity": "sha512-ZCsD6j97YD0mGU8/VnVs3NjORXa7zeHvqlpJpCqy4jU8a1O21dalL+MFn9QNbdEfy8rszR1N7NHeT7/LdtHf+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-int64": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sbd": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/sbd/-/sbd-1.0.5.tgz", + "integrity": "sha512-60PxBBWhg0C3yb5bTP+wwWYGTKMcuB0S6mTEa1sedMC79tYY0Ei7YjU4qsWzGn++lWscLQde16SnElJrf5/aTw==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitest/expect": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.1.tgz", + "integrity": "sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.1.tgz", + "integrity": "sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.1.tgz", + "integrity": "sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.1.tgz", + "integrity": "sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.1.1", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.1.tgz", + "integrity": "sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.1.tgz", + "integrity": "sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.1.tgz", + "integrity": "sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.1", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "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/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "devOptional": true, + "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==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/anymatch/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/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": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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-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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.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/aws-sigv4-fetch": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/aws-sigv4-fetch/-/aws-sigv4-fetch-4.4.1.tgz", + "integrity": "sha512-0eG8a6/LIoTMHbY1GRkZZfdRsQfKKEGS/IuApIyS/Jt+6mSJNKoQBHisjB4D7UGz+wDrZHeTKJgSKV1Yn1WFRA==", + "license": "MIT", + "dependencies": { + "aws-sigv4-sign": "1.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/aws-sigv4-sign": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/aws-sigv4-sign/-/aws-sigv4-sign-1.2.1.tgz", + "integrity": "sha512-iS0pV4xGzhexBCMG9ggXM5CaxTHa3KxOxkw2tphLgA/60vSycSWjJWso0s4xzGRrtABSi0b3LlxG5Jek7NjuqA==", + "license": "MIT", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-sdk/credential-provider-node": "^3.609.0", + "@smithy/protocol-http": "^4.0.3", + "@smithy/signature-v4": "^3.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/aws-sigv4-sign/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/aws-sigv4-sign/node_modules/@smithy/protocol-http": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.8.tgz", + "integrity": "sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/aws-sigv4-sign/node_modules/@smithy/signature-v4": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-3.1.2.tgz", + "integrity": "sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/aws-sigv4-sign/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/aws-sigv4-sign/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/aws-sigv4-sign/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/aws-sigv4-sign/node_modules/@smithy/util-middleware": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.11.tgz", + "integrity": "sha512-dWpyc1e1R6VoXrwLoLDd57U1z6CwNSdkM69Ie4+6uYh2GC7Vg51Qtan7ITzczuVpqezdDTKJGJB95fFvvjU/ow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/aws-sigv4-sign/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/aws-sigv4-sign/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, + "node_modules/aws4fetch": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/aws4fetch/-/aws4fetch-1.0.20.tgz", + "integrity": "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==", + "license": "MIT", + "optional": true + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" + }, + "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/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "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/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "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/bindings": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", + "integrity": "sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g==", + "license": "MIT", + "optional": true + }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "dev": true, + "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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/body-parser/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/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "license": "MIT" + }, + "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/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/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bson": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", + "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bufferutil": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "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/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "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/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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/cliui/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/cliui/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/cliui/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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cohere-ai": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/cohere-ai/-/cohere-ai-7.16.0.tgz", + "integrity": "sha512-hrG3EtVNSJLxJTaEeGRli+5rX34GiQC/UZ2WuUpaWiRwYbfzz7zKflfU/tg8SFFjkvYHDyS43UvVESepNd8C4w==", + "optional": true, + "dependencies": { + "@aws-sdk/client-sagemaker": "^3.583.0", + "@aws-sdk/credential-providers": "^3.583.0", + "@aws-sdk/protocol-http": "^3.374.0", + "@aws-sdk/signature-v4": "^3.374.0", + "convict": "^6.2.4", + "form-data": "^4.0.0", + "form-data-encoder": "^4.0.2", + "formdata-node": "^6.0.3", + "js-base64": "3.7.2", + "node-fetch": "2.7.0", + "qs": "6.11.2", + "readable-stream": "^4.5.2", + "url-join": "4.0.1" + } + }, + "node_modules/cohere-ai/node_modules/form-data-encoder": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", + "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cohere-ai/node_modules/formdata-node": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz", + "integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "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/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "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/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/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "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/convict": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.4.tgz", + "integrity": "sha512-qN60BAwdMVdofckX7AlohVJ2x9UvjTNoKVXCL2LxFk1l7757EJqf1nySdMkPQer0bt8kQ5lQiyZ9/2NvrFBuwQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "yargs-parser": "^20.2.7" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "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/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true, + "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/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "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/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "license": "MIT", + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "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/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/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/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-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "dev": true, + "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": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "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==", + "dev": true, + "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/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.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": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/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/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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/electron-to-chromium": { + "version": "1.5.129", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz", + "integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "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/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "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/esbuild": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, + "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": "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/escodegen/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/escodegen/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/escodegen/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/escodegen/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/escodegen/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/escodegen/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/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.", + "dev": true, + "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-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@jridgewell/sourcemap-codec": "^1.4.15", + "eslint-compat-utils": "^0.5.1", + "esutils": "^2.0.3", + "known-css-properties": "^0.35.0", + "postcss": "^8.4.38", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.1.0", + "semver": "^7.6.2", + "svelte-eslint-parser": "^0.43.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-svelte/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "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": "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/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz", + "integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "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==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "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/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "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/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.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/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/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/express/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/exsolve": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.4.tgz", + "integrity": "sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==", + "dev": true, + "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-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "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==", + "dev": true, + "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-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "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/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-event-stream": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/fetch-event-stream/-/fetch-event-stream-0.1.5.tgz", + "integrity": "sha512-V1PWovkspxQfssq/NnxoEyQo1DV+MRK/laPuPblIZmSjMN8P5u46OhlFQznSr9p/t0Sp8Uc6SbM3yCMfr0KU8g==", + "license": "MIT" + }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "19.6.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.6.0.tgz", + "integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==", + "license": "MIT", + "dependencies": { + "get-stream": "^9.0.1", + "strtok3": "^9.0.1", + "token-types": "^6.0.0", + "uint8array-extras": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "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==", + "dev": true, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "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/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "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/flatbuffers": { + "version": "25.2.10", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.2.10.tgz", + "integrity": "sha512-7JlN9ZvLDG1McO3kbX0k4v+SUAg48L1rIwEvN6ZQl/eCtgJz9UylTMzE9wrmYrcorgxm3CX/3T/w5VAub99UUw==", + "license": "Apache-2.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==", + "dev": true, + "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==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "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/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT", + "optional": true + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "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.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "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/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/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/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/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", + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/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", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gcp-metadata/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", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-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": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "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/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "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/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/graphql": { + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz", + "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/hash-wasm": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", + "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==", + "license": "MIT" + }, + "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/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "license": "MIT" + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "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-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/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/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "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/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.0.tgz", + "integrity": "sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "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-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "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.", + "dev": true, + "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/int53": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/int53/-/int53-0.2.4.tgz", + "integrity": "sha512-a5jlKftS7HUOhkUyYD7j2sJ/ZnvWiNlZS1ldR+g1ifQ+/UuZXIE+YTc/lK1qGj/GwAU5F8Z0e1eVq2t1J5Ob2g==", + "license": "BSD-3-Clause" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "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/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "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-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-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-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "license": "MIT" + }, + "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-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isomorphic-dompurify": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.22.0.tgz", + "integrity": "sha512-A2xsDNST1yB94rErEnwqlzSvGllCJ4e8lDMe1OWBH2hvpfc/2qzgMEiDshTO1HwO+PIDTiYeOc7ZDB7Ds49BOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dompurify": "^3.2.4", + "jsdom": "^26.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/cssstyle": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.0.tgz", + "integrity": "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.1.1", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/isomorphic-dompurify/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isomorphic-dompurify/node_modules/jsdom": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz", + "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.1", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/isomorphic-dompurify/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isomorphic-dompurify/node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/isomorphic-dompurify/node_modules/tr46": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz", + "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "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/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/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-base64": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", + "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/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/jsdom/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/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "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==", + "dev": true, + "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/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/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/katex": { + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/lint-staged": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.0.tgz", + "integrity": "sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/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/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/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/long": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", + "license": "Apache-2.0" + }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lzo": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/lzo/-/lzo-0.4.11.tgz", + "integrity": "sha512-apQHNoW2Alg72FMqaC/7pn03I7umdgSVFt2KRkCXXils4Z9u3QBh1uOtl2O5WmZIDLd9g6Lu4lIdOLmiSTFVCQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "~1.2.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "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/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/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "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/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/micromatch/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/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": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mongodb": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.15.0.tgz", + "integrity": "sha512-ifBhQ0rRzHDzqp9jAQP6OwHSH7dbYIQjD3SbJs9YYk9AikKEettW/9s/tbSFDTpXcRbF+u1aLrhHxDFaYtZpFQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.3", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/tr46": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz", + "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongodb-memory-server": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-10.1.4.tgz", + "integrity": "sha512-+oKQ/kc3CX+816oPFRtaF0CN4vNcGKNjpOQe4bHo/21A3pMD+lC7Xz1EX5HP7siCX4iCpVchDMmCOFXVQSGkUg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "mongodb-memory-server-core": "10.1.4", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-10.1.4.tgz", + "integrity": "sha512-o8fgY7ZalEd8pGps43fFPr/hkQu1L8i6HFEGbsTfA2zDOW0TopgpswaBCqDr0qD7ptibyPfB5DmC+UlIxbThzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-mutex": "^0.5.0", + "camelcase": "^6.3.0", + "debug": "^4.3.7", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.9", + "https-proxy-agent": "^7.0.5", + "mongodb": "^6.9.0", + "new-find-package-json": "^2.0.0", + "semver": "^7.6.3", + "tar-stream": "^3.1.7", + "tslib": "^2.7.0", + "yauzl": "^3.1.3" + }, + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/msw": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.3.tgz", + "integrity": "sha512-+mycXv8l2fEAjFZ5sjrtjJDmm2ceKGjrNbBr1durRg6VkU9fNUE/gsmQ51hWbHqs+l35W1iM+ZsmOD9Fd6lspw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.37.0", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" + }, + "node_modules/msw/node_modules/type-fest": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.39.0.tgz", + "integrity": "sha512-w2IGJU1tIgcrepg9ZJ82d8UmItNQtOFJG0HCUE3SzMokKkTsruVDALl2fAdiEzJlfduoU+VyXJWIIUZ+6jV+nw==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "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/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/new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "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/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/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "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-stream": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/object-stream/-/object-stream-0.0.1.tgz", + "integrity": "sha512-+NPJnRvX9RDMRY9mOWOo/NDppBjbZhXirNNSu2IBnuNboClC9h1ZGHXgHBLDbJMHsxeJDq922aVmG5xs24a/cA==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", + "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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/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": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.20.1.tgz", + "integrity": "sha512-YiU0s0IzYYC+gWvqD1HzLc46Du1sXpSiwzKb63PACIJr6LfL27VsXSXQvt68EzD3V0D5Bc0vyJTjmMxp0ylQiw==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.20.1.tgz", + "integrity": "sha512-di/I4HDXRw+FLgq+TyHmQEDd3cEp9iFFZm0r4uJ1Wd7b/WE1VXtKWo8yemex347c6GNF/3Pv86ZfPhIWxORr0w==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "onnxruntime-common": "1.20.1", + "tar": "^7.0.1" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.22.0-dev.20250306-ccf8fdd9ea", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250306-ccf8fdd9ea.tgz", + "integrity": "sha512-YwqS9Qqx2eKFXIx+HQloqRUG5/STHPUuNk8wn+qVVmwXBIfNdXX0/Lm7wgo5CnC2k+yqZmjDV5V1dZi4PeSPGQ==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.22.0-dev.20250306-aafa8d170a", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.22.0-dev.20250306-aafa8d170a", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250306-aafa8d170a.tgz", + "integrity": "sha512-NfIQnW4lIk/8LnhnYqknYPeet0U0+AADgKQRlKex36QrNoVSCY+aNaX6wyy2VzQ4CNWxsYh0E203ajRD/zxn0g==", + "license": "MIT" + }, + "node_modules/openai": { + "version": "4.91.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.91.0.tgz", + "integrity": "sha512-zdDg6eyvUmCP58QAW7/aPb+XdeavJ51pK6AcwZOWG5QNSLIovVz0XonRL9vARGJRmw8iImmvf2A31Q7hoh544w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.85", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.85.tgz", + "integrity": "sha512-61sLB7tXUMpkHJagZQAzPV4xGyqzulLvphe0lquRX80rZG24VupRv9p6Qo06V9VBNeGBM8Sv8rRVVLji6pi7QQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT", + "optional": true + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "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/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "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/package-manager-detector": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", + "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^0.2.7" + } + }, + "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/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parquetjs": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/parquetjs/-/parquetjs-0.11.2.tgz", + "integrity": "sha512-Y6FOc3Oi2AxY4TzJPz7fhICCR8tQNL3p+2xGQoUAMbmlJBR7+JJmMrwuyMjIpDiM7G8Wj/8oqOH4UDUmu4I5ZA==", + "license": "MIT", + "dependencies": { + "brotli": "^1.3.0", + "bson": "^1.0.4", + "int53": "^0.2.4", + "object-stream": "0.0.1", + "snappyjs": "^0.6.0", + "thrift": "^0.11.0", + "varint": "^5.0.0" + }, + "engines": { + "node": ">=7.6" + }, + "optionalDependencies": { + "lzo": "^0.4.0" + } + }, + "node_modules/parquetjs/node_modules/bson": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", + "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/peek-readable": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz", + "integrity": "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "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": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/pino": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz", + "integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.3.0.tgz", + "integrity": "sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", + "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", + "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "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.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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-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-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "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": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "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-nested/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-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "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/postcss/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/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz", + "integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz", + "integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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/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/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "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.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quansync": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", + "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "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/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "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/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": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "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/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-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.38.0.tgz", + "integrity": "sha512-5SsIRtJy9bf1ErAOiFMFzl64Ex9X5V7bnJ+WlFMb+zmP459OSWCEG7b0ERZ+PEU7xPt4OG3RHbrp1LJlXxYTrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.38.0", + "@rollup/rollup-android-arm64": "4.38.0", + "@rollup/rollup-darwin-arm64": "4.38.0", + "@rollup/rollup-darwin-x64": "4.38.0", + "@rollup/rollup-freebsd-arm64": "4.38.0", + "@rollup/rollup-freebsd-x64": "4.38.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.38.0", + "@rollup/rollup-linux-arm-musleabihf": "4.38.0", + "@rollup/rollup-linux-arm64-gnu": "4.38.0", + "@rollup/rollup-linux-arm64-musl": "4.38.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.38.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.38.0", + "@rollup/rollup-linux-riscv64-gnu": "4.38.0", + "@rollup/rollup-linux-riscv64-musl": "4.38.0", + "@rollup/rollup-linux-s390x-gnu": "4.38.0", + "@rollup/rollup-linux-x64-gnu": "4.38.0", + "@rollup/rollup-linux-x64-musl": "4.38.0", + "@rollup/rollup-win32-arm64-msvc": "4.38.0", + "@rollup/rollup-win32-ia32-msvc": "4.38.0", + "@rollup/rollup-win32-x64-msvc": "4.38.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "license": "MIT" + }, + "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/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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-html": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.15.0.tgz", + "integrity": "sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/satori": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.10.14.tgz", + "integrity": "sha512-abovcqmwl97WKioxpkfuMeZmndB1TuDFY/R+FymrZyiGP+pMYomvgSzVPnbNMWHHESOPosVHGL352oFbdAnJcA==", + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-to-react-native": "^3.0.0", + "emoji-regex": "^10.2.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-wasm-web": "^0.3.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/satori-html": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/satori-html/-/satori-html-0.3.2.tgz", + "integrity": "sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==", + "license": "MIT", + "dependencies": { + "ultrahtml": "^1.2.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/sbd": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sbd/-/sbd-1.0.19.tgz", + "integrity": "sha512-b5RyZMGSrFuIB4AHdbv12uYHS8YGEJ36gtuvG3RflbJGY+T0dXmAL0E4vZjQqT2RsX0v+ZwVqhV2zsGr5aFK9w==", + "license": "MIT", + "dependencies": { + "sanitize-html": "^2.3.2" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semiver": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz", + "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "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/serpapi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/serpapi/-/serpapi-1.1.1.tgz", + "integrity": "sha512-t5Bqu/6VMJ9naX8K+qCgUStpZOaNQFvIM4AudhMJLS6sqQT/EHaYrhGidDZHVx8QvcEdY6y1wNlxizOCtvJtUQ==", + "license": "MIT", + "dependencies": { + "undici": "^5.12.0" + } + }, + "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-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true, + "license": "MIT" + }, + "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/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "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/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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/snappyjs": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/snappyjs/-/snappyjs-0.6.1.tgz", + "integrity": "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg==", + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "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/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/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "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/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/std-env": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz", + "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", + "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "license": "MIT" + }, + "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-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-cjs/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/string-width/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/string-width/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/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, + "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-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/strtok3": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.1.1.tgz", + "integrity": "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.3.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.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/svelte": { + "version": "5.25.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.25.6.tgz", + "integrity": "sha512-RGkaeAXDuJdvhA1fdSM5GgD++vYfJYijZL0uN6kM2s/TRJ663jktBhZlF0qjzAJGR/34PtaeT3G8MKJY1EKeqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.6", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.5.tgz", + "integrity": "sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", + "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "postcss": "^8.4.39", + "postcss-scss": "^4.0.9" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-gestures": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/svelte-gestures/-/svelte-gestures-5.1.3.tgz", + "integrity": "sha512-ELOlzuH9E4+S1biCCTfusRlvzFpnqRPlljEqayoBTu5STH42u0kTT45D1m3Py3E9UmIyZTgrSLw6Fus/fh75Dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "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/tailwind-scrollbar": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.1.0.tgz", + "integrity": "sha512-pmrtDIZeHyu2idTejfV59SbaJyvp1VRjYxAjZBH0jnyrPRo6HL1kD5Glz8VPagasqr6oAx6M05+Tuw429Z8jxg==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "tailwindcss": "3.x" + } + }, + "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/tailwindcss/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/tailwindcss/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/tailwindcss/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/tailwindcss/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/tailwindcss/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/tailwindcss/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/tailwindcss/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/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bintrees": "1.0.2" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textlinestream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/textlinestream/-/textlinestream-1.1.1.tgz", + "integrity": "sha512-iBHbi7BQxrFmwZUQJsT0SjNzlLLsXhvW/kg7EyOMVMBIrlnj/qYofwo1LVLZi+3GbUEo96Iu2eqToI2+lZoAEQ==", + "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/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/thrift": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/thrift/-/thrift-0.11.0.tgz", + "integrity": "sha512-UpsBhOC45a45TpeHOXE4wwYwL8uD2apbHTbtBvkwtUU4dNwCjC7DpQTjw2Q6eIdfNtw+dKthdwq94uLXTJPfFw==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0", + "q": "^1.5.0", + "ws": ">= 2.2.3" + }, + "engines": { + "node": ">= 4.1.0" + } + }, + "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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.85", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.85.tgz", + "integrity": "sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.85" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.85", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.85.tgz", + "integrity": "sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==", + "license": "MIT" + }, + "node_modules/tldts-experimental": { + "version": "6.1.85", + "resolved": "https://registry.npmjs.org/tldts-experimental/-/tldts-experimental-6.1.85.tgz", + "integrity": "sha512-oM+m5GnOdxgbnfSfix98YvzAIgkKZbdqMD/BTLbnbL349MyaEeNo6z8jVX9/lrL6DvnjgW7RV+sIVojrFvB+hw==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.85" + } + }, + "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/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=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/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "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/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, + "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/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "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/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ultrahtml": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz", + "integrity": "sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==", + "license": "MIT" + }, + "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": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "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/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/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/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-icons": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.16.6.tgz", + "integrity": "sha512-jL70sAC7twp4hI/MTfm+vyvTRtHqiEIzf3XOjJz7yzhMEEQnk5Ey5YIXRAU03Mc4BF99ITvvnBzfyRZee86OeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^0.1.1", + "@antfu/utils": "^0.7.6", + "@iconify/utils": "^2.1.9", + "debug": "^4.3.4", + "kolorist": "^1.8.0", + "local-pkg": "^0.4.3", + "unplugin": "^1.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@svgr/core": ">=7.0.0", + "@svgx/core": "^1.0.1", + "@vue/compiler-sfc": "^3.0.2 || ^2.7.0", + "vue-template-compiler": "^2.6.12", + "vue-template-es2015-compiler": "^1.9.0" + }, + "peerDependenciesMeta": { + "@svgr/core": { + "optional": true + }, + "@svgx/core": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + }, + "vue-template-es2015-compiler": { + "optional": true + } + } + }, + "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==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "license": "MIT", + "optional": true + }, + "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/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": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/varint": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz", + "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==", + "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/vite": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz", + "integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.1.tgz", + "integrity": "sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitefu": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", + "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.1.tgz", + "integrity": "sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.1.1", + "@vitest/mocker": "3.1.1", + "@vitest/pretty-format": "^3.1.1", + "@vitest/runner": "3.1.1", + "@vitest/snapshot": "3.1.1", + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.2.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.8.1", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.1.1", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.1.1", + "@vitest/ui": "3.1.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/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/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "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/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "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/wrap-ansi-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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-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/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "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/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/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "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/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "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": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "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", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/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/yargs/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/yargs/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/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", + "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-wasm-web": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", + "license": "MIT" + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..39871258b273ed8caea19912967ba887c69cbcd5 --- /dev/null +++ b/package.json @@ -0,0 +1,124 @@ +{ + "name": "chat-ui", + "version": "0.9.4", + "private": true, + "packageManager": "npm@9.5.0", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check . && eslint .", + "format": "prettier --write .", + "test": "vitest", + "updateLocalEnv": "node --loader ts-node/esm scripts/updateLocalEnv.ts", + "populate": "vite-node --options.transformMode.ssr='/.*/' scripts/populate.ts", + "prepare": "husky" + }, + "devDependencies": { + "@faker-js/faker": "^8.4.1", + "@iconify-json/carbon": "^1.1.16", + "@iconify-json/eos-icons": "^1.1.6", + "@sveltejs/adapter-node": "^5.2.0", + "@sveltejs/kit": "^2.17.1", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/typography": "^0.5.9", + "@types/dompurify": "^3.0.5", + "@types/express": "^4.17.21", + "@types/js-yaml": "^4.0.9", + "@types/jsdom": "^21.1.1", + "@types/jsonpath": "^0.2.4", + "@types/katex": "^0.16.7", + "@types/mime-types": "^2.1.4", + "@types/minimist": "^1.2.5", + "@types/node": "^22.1.0", + "@types/parquetjs": "^0.10.3", + "@types/sbd": "^1.0.5", + "@types/uuid": "^9.0.8", + "@typescript-eslint/eslint-plugin": "^6.x", + "@typescript-eslint/parser": "^6.x", + "dompurify": "^3.2.4", + "eslint": "^8.28.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-svelte": "^2.45.1", + "isomorphic-dompurify": "^2.13.0", + "js-yaml": "^4.1.0", + "minimist": "^1.2.8", + "mongodb-memory-server": "^10.1.2", + "prettier": "^3.1.0", + "prettier-plugin-svelte": "^3.2.6", + "prettier-plugin-tailwindcss": "^0.6.11", + "prom-client": "^15.1.2", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "svelte-gestures": "^5.1.3", + "ts-node": "^10.9.1", + "tslib": "^2.4.1", + "typescript": "^5.5.0", + "unplugin-icons": "^0.16.1", + "vite": "^6.2.3", + "vite-node": "^3.0.9", + "vitest": "^3.0.9" + }, + "type": "module", + "dependencies": { + "@aws-sdk/credential-providers": "^3.592.0", + "@cliqz/adblocker-playwright": "^1.27.2", + "@gradio/client": "^1.8.0", + "@huggingface/hub": "^0.5.1", + "@huggingface/inference": "^2.8.1", + "@huggingface/transformers": "^3.1.1", + "@iconify-json/bi": "^1.1.21", + "@playwright/browser-chromium": "^1.43.1", + "@resvg/resvg-js": "^2.6.2", + "autoprefixer": "^10.4.14", + "aws-sigv4-fetch": "^4.0.1", + "aws4": "^1.13.0", + "date-fns": "^2.29.3", + "dotenv": "^16.0.3", + "express": "^4.21.2", + "file-type": "^19.4.1", + "google-auth-library": "^9.13.0", + "handlebars": "^4.7.8", + "highlight.js": "^11.7.0", + "husky": "^9.0.11", + "image-size": "^1.0.2", + "ip-address": "^9.0.5", + "jose": "^5.3.0", + "jsdom": "^22.0.0", + "json5": "^2.2.3", + "jsonpath": "^1.1.1", + "katex": "^0.16.21", + "lint-staged": "^15.2.7", + "marked": "^12.0.1", + "mongodb": "^6.14.2", + "nanoid": "^5.0.9", + "openid-client": "^5.4.2", + "parquetjs": "^0.11.2", + "pino": "^9.0.0", + "pino-pretty": "^11.0.0", + "playwright": "^1.44.1", + "postcss": "^8.4.31", + "saslprep": "^1.0.3", + "satori": "^0.10.11", + "satori-html": "^0.3.2", + "sbd": "^1.0.19", + "serpapi": "^1.1.1", + "sharp": "^0.33.4", + "tailwind-scrollbar": "^3.0.0", + "tailwindcss": "^3.4.0", + "uuid": "^10.0.0", + "zod": "^3.22.3" + }, + "optionalDependencies": { + "@anthropic-ai/sdk": "^0.32.1", + "@anthropic-ai/vertex-sdk": "^0.4.1", + "@aws-sdk/client-bedrock-runtime": "^3.631.0", + "@google-cloud/vertexai": "^1.1.0", + "@google/generative-ai": "^0.14.1", + "aws4fetch": "^1.0.17", + "cohere-ai": "^7.9.0", + "openai": "^4.44.0" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..7b75c83aff1c05e0e0e315638e07a22314603d4d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/scripts/populate.ts b/scripts/populate.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d93d1dfe8c6e277e111406d5687ae5eb8757e99 --- /dev/null +++ b/scripts/populate.ts @@ -0,0 +1,388 @@ +import readline from "readline"; +import minimist from "minimist"; + +// @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them +import { env } from "$env/dynamic/private"; + +import { faker } from "@faker-js/faker"; +import { ObjectId } from "mongodb"; + +// @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them +import { collections } from "$lib/server/database"; +import { models } from "../src/lib/server/models.ts"; +import type { User } from "../src/lib/types/User"; +import type { Assistant } from "../src/lib/types/Assistant"; +import type { Conversation } from "../src/lib/types/Conversation"; +import type { Settings } from "../src/lib/types/Settings"; +import type { CommunityToolDB, ToolLogoColor, ToolLogoIcon } from "../src/lib/types/Tool"; +import { defaultEmbeddingModel } from "../src/lib/server/embeddingModels.ts"; +import { Message } from "../src/lib/types/Message.ts"; + +import { addChildren } from "../src/lib/utils/tree/addChildren.ts"; +import { generateSearchTokens } from "../src/lib/utils/searchTokens.ts"; +import { ReviewStatus } from "../src/lib/types/Review.ts"; +import fs from "fs"; +import path from "path"; +import { MessageUpdateType } from "../src/lib/types/MessageUpdate.ts"; +import { MessageReasoningUpdateType } from "../src/lib/types/MessageUpdate.ts"; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +rl.on("close", function () { + process.exit(0); +}); + +const samples = fs.readFileSync(path.join(__dirname, "samples.txt"), "utf8").split("\n---\n"); + +const possibleFlags = ["reset", "all", "users", "settings", "assistants", "conversations", "tools"]; +const argv = minimist(process.argv.slice(2)); +const flags = argv["_"].filter((flag) => possibleFlags.includes(flag)); + +async function generateMessages(preprompt?: string): Promise { + const isLinear = faker.datatype.boolean(0.5); + const isInterrupted = faker.datatype.boolean(0.05); + + const messages: Message[] = []; + + messages.push({ + id: crypto.randomUUID(), + from: "system", + content: preprompt ?? "", + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + }); + + let isUser = true; + let lastId = messages[0].id; + if (isLinear) { + const convLength = faker.number.int({ min: 1, max: 25 }) * 2; // must always be even + + for (let i = 0; i < convLength; i++) { + const hasReasoning = Math.random() < 0.2; + lastId = addChildren( + { + messages, + rootMessageId: messages[0].id, + }, + { + from: isUser ? "user" : "assistant", + content: + faker.lorem.sentence({ + min: 10, + max: isUser ? 50 : 200, + }) + + (!isUser && Math.random() < 0.1 + ? "\n```\n" + faker.helpers.arrayElement(samples) + "\n```\n" + : ""), + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + reasoning: hasReasoning ? faker.lorem.paragraphs(2) : undefined, + updates: hasReasoning + ? [ + { + type: MessageUpdateType.Reasoning, + subtype: MessageReasoningUpdateType.Status, + uuid: crypto.randomUUID(), + status: "thinking", + }, + ] + : [], + interrupted: !isUser && i === convLength - 1 && isInterrupted, + }, + lastId + ); + isUser = !isUser; + } + } else { + const convLength = faker.number.int({ min: 2, max: 200 }); + + for (let i = 0; i < convLength; i++) { + const hasReasoning = Math.random() < 0.2; + addChildren( + { + messages, + rootMessageId: messages[0].id, + }, + { + from: isUser ? "user" : "assistant", + content: + faker.lorem.sentence({ + min: 10, + max: isUser ? 50 : 200, + }) + + (!isUser && Math.random() < 0.1 + ? "\n```\n" + faker.helpers.arrayElement(samples) + "\n```\n" + : ""), + reasoning: hasReasoning ? faker.lorem.paragraphs(2) : undefined, + updates: hasReasoning + ? [ + { + type: MessageUpdateType.Reasoning, + subtype: MessageReasoningUpdateType.Status, + uuid: crypto.randomUUID(), + status: "thinking", + }, + ] + : [], + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + interrupted: !isUser && i === convLength - 1 && isInterrupted, + }, + faker.helpers.arrayElement([ + messages[0].id, + ...messages.filter((m) => m.from === (isUser ? "assistant" : "user")).map((m) => m.id), + ]) + ); + + isUser = !isUser; + } + } + return messages; +} + +async function seed() { + console.log("Seeding..."); + const modelIds = models.map((model) => model.id); + + if (flags.includes("reset")) { + console.log("Starting reset of DB"); + await collections.users.deleteMany({}); + await collections.settings.deleteMany({}); + await collections.assistants.deleteMany({}); + await collections.conversations.deleteMany({}); + await collections.tools.deleteMany({}); + await collections.migrationResults.deleteMany({}); + await collections.semaphores.deleteMany({}); + console.log("Reset done"); + } + + if (flags.includes("users") || flags.includes("all")) { + console.log("Creating 100 new users"); + const newUsers: User[] = Array.from({ length: 100 }, () => ({ + _id: new ObjectId(), + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + username: faker.internet.userName(), + name: faker.person.fullName(), + hfUserId: faker.string.alphanumeric(24), + avatarUrl: faker.image.avatar(), + })); + + await collections.users.insertMany(newUsers); + console.log("Done creating users."); + } + + const users = await collections.users.find().toArray(); + if (flags.includes("settings") || flags.includes("all")) { + console.log("Updating settings for all users"); + users.forEach(async (user) => { + const settings: Settings = { + userId: user._id, + shareConversationsWithModelAuthors: faker.datatype.boolean(0.25), + hideEmojiOnSidebar: faker.datatype.boolean(0.25), + ethicsModalAcceptedAt: faker.date.recent({ days: 30 }), + activeModel: faker.helpers.arrayElement(modelIds), + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + disableStream: faker.datatype.boolean(0.25), + directPaste: faker.datatype.boolean(0.25), + customPrompts: {}, + assistants: [], + }; + await collections.settings.updateOne( + { userId: user._id }, + { $set: { ...settings } }, + { upsert: true } + ); + }); + console.log("Done updating settings."); + } + + if (flags.includes("assistants") || flags.includes("all")) { + console.log("Creating assistants for all users"); + await Promise.all( + users.map(async (user) => { + const name = faker.animal.insect(); + const assistants = faker.helpers.multiple( + () => ({ + _id: new ObjectId(), + name, + createdById: user._id, + createdByName: user.username, + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + userCount: faker.number.int({ min: 1, max: 100000 }), + review: faker.helpers.enumValue(ReviewStatus), + modelId: faker.helpers.arrayElement(modelIds), + description: faker.lorem.sentence(), + preprompt: faker.hacker.phrase(), + exampleInputs: faker.helpers.multiple(() => faker.lorem.sentence(), { + count: faker.number.int({ min: 0, max: 4 }), + }), + searchTokens: generateSearchTokens(name), + last24HoursCount: faker.number.int({ min: 0, max: 1000 }), + }), + { count: faker.number.int({ min: 3, max: 10 }) } + ); + await collections.assistants.insertMany(assistants); + await collections.settings.updateOne( + { userId: user._id }, + { $set: { assistants: assistants.map((a) => a._id.toString()) } }, + { upsert: true } + ); + }) + ); + console.log("Done creating assistants."); + } + + if (flags.includes("conversations") || flags.includes("all")) { + console.log("Creating conversations for all users"); + await Promise.all( + users.map(async (user) => { + const conversations = faker.helpers.multiple( + async () => { + const settings = await collections.settings.findOne({ userId: user._id }); + + const assistantId = + settings?.assistants && settings.assistants.length > 0 && faker.datatype.boolean(0.1) + ? faker.helpers.arrayElement(settings.assistants) + : undefined; + + const preprompt = + (assistantId + ? await collections.assistants + .findOne({ _id: assistantId }) + .then((assistant: Assistant) => assistant?.preprompt ?? "") + : faker.helpers.maybe(() => faker.hacker.phrase(), { probability: 0.5 })) ?? ""; + + const messages = await generateMessages(preprompt); + + const conv = { + _id: new ObjectId(), + userId: user._id, + assistantId, + preprompt, + createdAt: faker.date.recent({ days: 145 }), + updatedAt: faker.date.recent({ days: 145 }), + model: faker.helpers.arrayElement(modelIds), + title: faker.internet.emoji() + " " + faker.hacker.phrase(), + embeddingModel: defaultEmbeddingModel.id, + messages, + rootMessageId: messages[0].id, + } satisfies Conversation; + + return conv; + }, + { count: faker.number.int({ min: 10, max: 200 }) } + ); + + await collections.conversations.insertMany(await Promise.all(conversations)); + }) + ); + console.log("Done creating conversations."); + } + + // generate Community Tools + if (flags.includes("tools") || flags.includes("all")) { + const tools = await Promise.all( + faker.helpers.multiple( + () => { + const _id = new ObjectId(); + const displayName = faker.company.catchPhrase(); + const description = faker.company.catchPhrase(); + const color = faker.helpers.arrayElement([ + "purple", + "blue", + "green", + "yellow", + "red", + ]) satisfies ToolLogoColor; + const icon = faker.helpers.arrayElement([ + "wikis", + "tools", + "camera", + "code", + "email", + "cloud", + "terminal", + "game", + "chat", + "speaker", + "video", + ]) satisfies ToolLogoIcon; + const baseUrl = faker.helpers.arrayElement([ + "stabilityai/stable-diffusion-3-medium", + "multimodalart/cosxl", + "gokaygokay/SD3-Long-Captioner", + "xichenhku/MimicBrush", + ]); + + // keep empty for populate for now + + const user: User = faker.helpers.arrayElement(users); + const createdById = user._id; + const createdByName = user.username ?? user.name; + + return { + type: "community" as const, + _id, + createdById, + createdByName, + displayName, + name: displayName.toLowerCase().replace(" ", "_"), + endpoint: "/test", + description, + color, + icon, + baseUrl, + inputs: [], + outputPath: null, + outputType: "str" as const, + showOutput: false, + useCount: faker.number.int({ min: 0, max: 100000 }), + last24HoursUseCount: faker.number.int({ min: 0, max: 1000 }), + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + searchTokens: generateSearchTokens(displayName), + review: faker.helpers.enumValue(ReviewStatus), + outputComponent: null, + outputComponentIdx: null, + }; + }, + { count: faker.number.int({ min: 10, max: 200 }) } + ) + ); + + await collections.tools.insertMany(tools satisfies CommunityToolDB[]); + } +} + +// run seed +(async () => { + try { + rl.question( + "You're about to run a seeding script on the following MONGODB_URL: \x1b[31m" + + env.MONGODB_URL + + "\x1b[0m\n\n With the following flags: \x1b[31m" + + flags.join("\x1b[0m , \x1b[31m") + + "\x1b[0m\n \n\n Are you sure you want to continue? (yes/no): ", + async (confirm) => { + if (confirm !== "yes") { + console.log("Not 'yes', exiting."); + rl.close(); + process.exit(0); + } + console.log("Starting seeding..."); + await seed(); + console.log("Seeding done."); + rl.close(); + } + ); + } catch (e) { + console.error(e); + process.exit(1); + } +})(); diff --git a/scripts/samples.txt b/scripts/samples.txt new file mode 100644 index 0000000000000000000000000000000000000000..acca18ac4ee3bca49e6a4c59a380dc5b635703a9 --- /dev/null +++ b/scripts/samples.txt @@ -0,0 +1,194 @@ +import { Observable, of, from, interval, throwError } from 'rxjs'; +import { map, filter, catchError, switchMap, take, tap } from 'rxjs/operators'; + +// Mock function to fetch stock prices (simulates API call) +const fetchStockPrice = (ticker: string): Observable => { + return new Observable((observer) => { + const intervalId = setInterval(() => { + if (Math.random() < 0.1) { // Simulating an error 10% of the time + observer.error(`Error fetching stock price for ${ticker}`); + } else { + const price = parseFloat((Math.random() * 1000).toFixed(2)); + observer.next(price); + } + }, 1000); + + return () => { + clearInterval(intervalId); + console.log(`Stopped fetching prices for ${ticker}`); + }; + }); +}; + +// Example usage: Tracking stock price updates +const stockTicker = 'AAPL'; +const stockPrice$ = fetchStockPrice(stockTicker).pipe( + map(price => ({ ticker: stockTicker, price })), // Transform data + filter(data => data.price > 500), // Only keep prices above 500 + tap(data => console.log(`Price update:`, data)), // Side effect: Logging + catchError(err => { + console.error(err); + return of({ ticker: stockTicker, price: null }); // Fallback observable + }) +); + +// Subscribe to the stock price updates +const subscription = stockPrice$.subscribe({ + next: data => console.log(`Subscriber received:`, data), + error: err => console.error(`Subscription error:`, err), + complete: () => console.log('Stream complete'), +}); + +// Automatically unsubscribe after 10 seconds +setTimeout(() => { + subscription.unsubscribe(); + console.log('Unsubscribed from stock price updates.'); +}, 10000); +--- +class EnforceAttrsMeta(type): + """ + Metaclass that enforces the presence of specific attributes in a class + and automatically decorates methods with a logging wrapper. + """ + + required_attributes = ['name', 'version'] + + def __new__(cls, name, bases, class_dict): + """ + Create a new class with enforced attributes and method logging. + + :param name: Name of the class being created. + :param bases: Tuple of base classes. + :param class_dict: Dictionary of attributes and methods of the class. + :return: Newly created class object. + """ + # Ensure required attributes exist + for attr in cls.required_attributes: + if attr not in class_dict: + raise TypeError(f"Class '{name}' is missing required attribute '{attr}'") + + # Wrap all methods in a logging decorator + for key, value in class_dict.items(): + if callable(value): # Check if it's a method + class_dict[key] = cls.log_calls(value) + + return super().__new__(cls, name, bases, class_dict) + + @staticmethod + def log_calls(func): + """ + Decorator that logs method calls and arguments. + + :param func: Function to be wrapped. + :return: Wrapped function with logging. + """ + def wrapper(*args, **kwargs): + print(f"Calling {func.__name__} with args={args} kwargs={kwargs}") + result = func(*args, **kwargs) + print(f"{func.__name__} returned {result}") + return result + return wrapper + + +class PluginBase(metaclass=EnforceAttrsMeta): + """ + Base class for plugins that enforces required attributes and logging. + """ + name = "BasePlugin" + version = "1.0" + + def run(self, data): + """ + Process the input data. + + :param data: The data to be processed. + :return: Processed result. + """ + return f"Processed {data}" + + +class CustomPlugin(PluginBase): + """ + Custom plugin that extends PluginBase and adheres to enforced rules. + """ + name = "CustomPlugin" + version = "2.0" + + def run(self, data): + """ + Custom processing logic. + + :param data: The data to process. + :return: Modified data. + """ + return f"Custom processing of {data}" + + +# Uncommenting the following class definition will raise a TypeError +# because 'version' attribute is missing. +# class InvalidPlugin(PluginBase): +# name = "InvalidPlugin" + + +if __name__ == "__main__": + # Instantiate and use the plugin + plugin = CustomPlugin() + print(plugin.run("example data")) +--- + + + + + + Click the Box Game + + + +

Click the Box!

+

Score: 0

+
+
+
+ + + diff --git a/scripts/setupTest.ts b/scripts/setupTest.ts new file mode 100644 index 0000000000000000000000000000000000000000..bea2c2833104816e602214cdae1b7e548fdbefcf --- /dev/null +++ b/scripts/setupTest.ts @@ -0,0 +1,49 @@ +import { vi, afterAll } from "vitest"; +import dotenv from "dotenv"; +import { resolve } from "path"; +import fs from "fs"; +import { MongoMemoryServer } from "mongodb-memory-server"; + +let mongoServer: MongoMemoryServer; +// Load the .env file +const envPath = resolve(__dirname, "../.env"); +dotenv.config({ path: envPath }); + +// Read the .env file content +const envContent = fs.readFileSync(envPath, "utf-8"); + +// Parse the .env content +const envVars = dotenv.parse(envContent); + +// Separate public and private variables +const publicEnv = {}; +const privateEnv = {}; + +for (const [key, value] of Object.entries(envVars)) { + if (key.startsWith("PUBLIC_")) { + publicEnv[key] = value; + } else { + privateEnv[key] = value; + } +} + +vi.mock("$env/dynamic/public", () => ({ + env: publicEnv, +})); + +vi.mock("$env/dynamic/private", async () => { + mongoServer = await MongoMemoryServer.create(); + + return { + env: { + ...privateEnv, + MONGODB_URL: mongoServer.getUri(), + }, + }; +}); + +afterAll(async () => { + if (mongoServer) { + await mongoServer.stop(); + } +}); diff --git a/scripts/updateLocalEnv.ts b/scripts/updateLocalEnv.ts new file mode 100644 index 0000000000000000000000000000000000000000..998cc705f476126de1cf06561687577e018f239b --- /dev/null +++ b/scripts/updateLocalEnv.ts @@ -0,0 +1,37 @@ +import fs from "fs"; +import yaml from "js-yaml"; + +const file = fs.readFileSync("chart/env/prod.yaml", "utf8"); + +// have to do a weird stringify/parse because of some node error +const prod = JSON.parse(JSON.stringify(yaml.load(file))); +const vars = prod.envVars as Record; + +let PUBLIC_CONFIG = ""; + +Object.entries(vars) + // filter keys used in prod with the proxy + .filter(([key]) => !["XFF_DEPTH", "ADDRESS_HEADER"].includes(key)) + .forEach(([key, value]) => { + PUBLIC_CONFIG += `${key}=\`${value}\`\n`; + }); + +const SECRET_CONFIG = + (fs.existsSync(".env.SECRET_CONFIG") + ? fs.readFileSync(".env.SECRET_CONFIG", "utf8") + : process.env.SECRET_CONFIG) ?? ""; + +// Prepend the content of the env variable SECRET_CONFIG +let full_config = `${PUBLIC_CONFIG}\n${SECRET_CONFIG}`; + +// replace the internal proxy url with the public endpoint +full_config = full_config.replaceAll( + "https://internal.api-inference.huggingface.co", + "https://router.huggingface.co/hf-inference" +); + +full_config = full_config.replaceAll("COOKIE_SECURE=`true`", "COOKIE_SECURE=`false`"); +full_config = full_config.replaceAll("LOG_LEVEL=`debug`", "LOG_LEVEL=`info`"); + +// Write full_config to .env.local +fs.writeFileSync(".env.local", full_config); diff --git a/src/ambient.d.ts b/src/ambient.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..10d56a1ad2da5a0ea32e2180c6aeb04d39290b8c --- /dev/null +++ b/src/ambient.d.ts @@ -0,0 +1,4 @@ +declare module "*.ttf" { + const value: ArrayBuffer; + export default value; +} diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..40a38728d8f8a7dcef594f49adbe5bff7294705b --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,25 @@ +/// +/// + +import type { User } from "$lib/types/User"; + +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + interface Locals { + sessionId: string; + user?: User & { logoutDisabled?: boolean }; + } + + interface Error { + message: string; + errorId?: ReturnType; + } + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000000000000000000000000000000000000..1bf87c74f9fc1402320865bf498eda3219d95146 --- /dev/null +++ b/src/app.html @@ -0,0 +1,47 @@ + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + + + + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..964d53c625448d472b08ed25c17addd611752bbc --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,303 @@ +import { env } from "$env/dynamic/private"; +import { env as envPublic } from "$env/dynamic/public"; +import type { Handle, HandleServerError } from "@sveltejs/kit"; +import { collections } from "$lib/server/database"; +import { base } from "$app/paths"; +import { findUser, refreshSessionCookie, requiresUser } from "$lib/server/auth"; +import { ERROR_MESSAGES } from "$lib/stores/errors"; +import { sha256 } from "$lib/utils/sha256"; +import { addWeeks } from "date-fns"; +import { checkAndRunMigrations } from "$lib/migrations/migrations"; +import { building } from "$app/environment"; +import { logger } from "$lib/server/logger"; +import { AbortedGenerations } from "$lib/server/abortedGenerations"; +import { MetricsServer } from "$lib/server/metrics"; +import { initExitHandler } from "$lib/server/exitHandler"; +import { ObjectId } from "mongodb"; +import { refreshAssistantsCounts } from "$lib/jobs/refresh-assistants-counts"; +import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats"; + +// TODO: move this code on a started server hook, instead of using a "building" flag +if (!building) { + // Set HF_TOKEN as a process variable for Transformers.JS to see it + process.env.HF_TOKEN ??= env.HF_TOKEN; + + logger.info("Starting server..."); + initExitHandler(); + + checkAndRunMigrations(); + if (env.ENABLE_ASSISTANTS) { + refreshAssistantsCounts(); + } + refreshConversationStats(); + + // Init metrics server + MetricsServer.getInstance(); + + // Init AbortedGenerations refresh process + AbortedGenerations.getInstance(); + + if (env.EXPOSE_API) { + logger.warn( + "The EXPOSE_API flag has been deprecated. The API is now required for chat-ui to work." + ); + } +} + +export const handleError: HandleServerError = async ({ error, event, status, message }) => { + // handle 404 + + if (building) { + throw error; + } + + if (event.route.id === null) { + return { + message: `Page ${event.url.pathname} not found`, + }; + } + + const errorId = crypto.randomUUID(); + + logger.error({ + locals: event.locals, + url: event.request.url, + params: event.params, + request: event.request, + message, + error, + errorId, + status, + stack: error instanceof Error ? error.stack : undefined, + }); + + return { + message: "An error occurred", + errorId, + }; +}; + +export const handle: Handle = async ({ event, resolve }) => { + logger.debug({ + locals: event.locals, + url: event.url.pathname, + params: event.params, + request: event.request, + }); + + function errorResponse(status: number, message: string) { + const sendJson = + event.request.headers.get("accept")?.includes("application/json") || + event.request.headers.get("content-type")?.includes("application/json"); + return new Response(sendJson ? JSON.stringify({ error: message }) : message, { + status, + headers: { + "content-type": sendJson ? "application/json" : "text/plain", + }, + }); + } + + if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) { + const ADMIN_SECRET = env.ADMIN_API_SECRET || env.PARQUET_EXPORT_SECRET; + + if (!ADMIN_SECRET) { + return errorResponse(500, "Admin API is not configured"); + } + + if (event.request.headers.get("Authorization") !== `Bearer ${ADMIN_SECRET}`) { + return errorResponse(401, "Unauthorized"); + } + } + + const token = event.cookies.get(env.COOKIE_NAME); + + // if the trusted email header is set we use it to get the user email + const email = env.TRUSTED_EMAIL_HEADER + ? event.request.headers.get(env.TRUSTED_EMAIL_HEADER) + : null; + + let secretSessionId: string | null = null; + let sessionId: string | null = null; + + if (email) { + secretSessionId = sessionId = await sha256(email); + + event.locals.user = { + // generate id based on email + _id: new ObjectId(sessionId.slice(0, 24)), + name: email, + email, + createdAt: new Date(), + updatedAt: new Date(), + hfUserId: email, + avatarUrl: "", + logoutDisabled: true, + }; + } else if (token) { + secretSessionId = token; + sessionId = await sha256(token); + + const user = await findUser(sessionId); + + if (user) { + event.locals.user = user; + } + } else if (event.url.pathname.startsWith(`${base}/api/`) && env.USE_HF_TOKEN_IN_API === "true") { + // if the request goes to the API and no user is available in the header + // check if a bearer token is available in the Authorization header + + const authorization = event.request.headers.get("Authorization"); + + if (authorization && authorization.startsWith("Bearer ")) { + const token = authorization.slice(7); + + const hash = await sha256(token); + + sessionId = secretSessionId = hash; + + // check if the hash is in the DB and get the user + // else check against https://huggingface.co/api/whoami-v2 + + const cacheHit = await collections.tokenCaches.findOne({ tokenHash: hash }); + + if (cacheHit) { + const user = await collections.users.findOne({ hfUserId: cacheHit.userId }); + + if (!user) { + return errorResponse(500, "User not found"); + } + + event.locals.user = user; + } else { + const response = await fetch("https://huggingface.co/api/whoami-v2", { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + return errorResponse(401, "Unauthorized"); + } + + const data = await response.json(); + const user = await collections.users.findOne({ hfUserId: data.id }); + + if (!user) { + return errorResponse(500, "User not found"); + } + + await collections.tokenCaches.insertOne({ + tokenHash: hash, + userId: data.id, + createdAt: new Date(), + updatedAt: new Date(), + }); + + event.locals.user = user; + } + } + } + + if (!sessionId || !secretSessionId) { + secretSessionId = crypto.randomUUID(); + sessionId = await sha256(secretSessionId); + + if (await collections.sessions.findOne({ sessionId })) { + return errorResponse(500, "Session ID collision"); + } + } + + event.locals.sessionId = sessionId; + + // CSRF protection + const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? ""; + /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */ + const nativeFormContentTypes = [ + "multipart/form-data", + "application/x-www-form-urlencoded", + "text/plain", + ]; + + if (event.request.method === "POST") { + if (nativeFormContentTypes.includes(requestContentType)) { + const origin = event.request.headers.get("origin"); + + if (!origin) { + return errorResponse(403, "Non-JSON form requests need to have an origin"); + } + + const validOrigins = [ + new URL(event.request.url).host, + ...(envPublic.PUBLIC_ORIGIN ? [new URL(envPublic.PUBLIC_ORIGIN).host] : []), + ]; + + if (!validOrigins.includes(new URL(origin).host)) { + return errorResponse(403, "Invalid referer for POST request"); + } + } + } + + if (event.request.method === "POST") { + // if the request is a POST request we refresh the cookie + refreshSessionCookie(event.cookies, secretSessionId); + + await collections.sessions.updateOne( + { sessionId }, + { $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } } + ); + } + + if ( + !event.url.pathname.startsWith(`${base}/login`) && + !event.url.pathname.startsWith(`${base}/admin`) && + !event.url.pathname.startsWith(`${base}/settings`) && + !["GET", "OPTIONS", "HEAD"].includes(event.request.method) + ) { + if ( + !event.locals.user && + requiresUser && + !((env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0) > 0) + ) { + return errorResponse(401, ERROR_MESSAGES.authOnly); + } + + // if login is not required and the call is not from /settings and we display the ethics modal with PUBLIC_APP_DISCLAIMER + // we check if the user has accepted the ethics modal first. + // If login is required, `ethicsModalAcceptedAt` is already true at this point, so do not pass this condition. This saves a DB call. + if ( + !requiresUser && + !event.url.pathname.startsWith(`${base}/settings`) && + envPublic.PUBLIC_APP_DISCLAIMER === "1" + ) { + const hasAcceptedEthicsModal = await collections.settings.countDocuments({ + sessionId: event.locals.sessionId, + ethicsModalAcceptedAt: { $exists: true }, + }); + + if (!hasAcceptedEthicsModal) { + return errorResponse(405, "You need to accept the welcome modal first"); + } + } + } + + let replaced = false; + + const response = await resolve(event, { + transformPageChunk: (chunk) => { + // For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template + if (replaced || !chunk.html.includes("%gaId%")) { + return chunk.html; + } + replaced = true; + + return chunk.html.replace("%gaId%", envPublic.PUBLIC_GOOGLE_ANALYTICS_ID); + }, + }); + + // Add CSP header to disallow framing if ALLOW_IFRAME is not "true" + if (env.ALLOW_IFRAME !== "true") { + response.headers.append("Content-Security-Policy", "frame-ancestors 'none';"); + } + + return response; +}; diff --git a/src/lib/actions/clickOutside.ts b/src/lib/actions/clickOutside.ts new file mode 100644 index 0000000000000000000000000000000000000000..6aa146932fea03a06fa3c654c58460d33c389850 --- /dev/null +++ b/src/lib/actions/clickOutside.ts @@ -0,0 +1,18 @@ +export function clickOutside(element: HTMLElement, callbackFunction: () => void) { + function onClick(event: MouseEvent) { + if (!element.contains(event.target as Node)) { + callbackFunction(); + } + } + + document.body.addEventListener("click", onClick); + + return { + update(newCallbackFunction: () => void) { + callbackFunction = newCallbackFunction; + }, + destroy() { + document.body.removeEventListener("click", onClick); + }, + }; +} diff --git a/src/lib/actions/snapScrollToBottom.ts b/src/lib/actions/snapScrollToBottom.ts new file mode 100644 index 0000000000000000000000000000000000000000..b22a0648221f6b58853a910fb6286f79574a0246 --- /dev/null +++ b/src/lib/actions/snapScrollToBottom.ts @@ -0,0 +1,54 @@ +import { navigating } from "$app/stores"; +import { tick } from "svelte"; +import { get } from "svelte/store"; + +const detachedOffset = 10; + +/** + * @param node element to snap scroll to bottom + * @param dependency pass in a dependency to update scroll on changes. + */ +export const snapScrollToBottom = (node: HTMLElement, dependency: unknown) => { + let prevScrollValue = node.scrollTop; + let isDetached = false; + + const handleScroll = () => { + // if user scrolled up, we detach + if (node.scrollTop < prevScrollValue) { + isDetached = true; + } + + // if user scrolled back to within 10px of bottom, we reattach + if (node.scrollTop - (node.scrollHeight - node.clientHeight) >= -detachedOffset) { + isDetached = false; + } + + prevScrollValue = node.scrollTop; + }; + + const updateScroll = async (_options: { force?: boolean } = {}) => { + const defaultOptions = { force: false }; + const options = { ...defaultOptions, ..._options }; + const { force } = options; + + if (!force && isDetached && !get(navigating)) return; + + // wait for next tick to ensure that the DOM is updated + await tick(); + + node.scrollTo({ top: node.scrollHeight }); + }; + + node.addEventListener("scroll", handleScroll); + + if (dependency) { + updateScroll({ force: true }); + } + + return { + update: updateScroll, + destroy: () => { + node.removeEventListener("scroll", handleScroll); + }, + }; +}; diff --git a/src/lib/buildPrompt.ts b/src/lib/buildPrompt.ts new file mode 100644 index 0000000000000000000000000000000000000000..1659d55130aadd3b4e98b1b61c12ac8e31816490 --- /dev/null +++ b/src/lib/buildPrompt.ts @@ -0,0 +1,56 @@ +import type { EndpointParameters } from "./server/endpoints/endpoints"; +import type { BackendModel } from "./server/models"; +import type { Tool, ToolResult } from "./types/Tool"; + +type buildPromptOptions = Pick & { + model: BackendModel; + tools?: Tool[]; + toolResults?: ToolResult[]; +}; + +export async function buildPrompt({ + messages, + model, + preprompt, + continueMessage, + tools, + toolResults, +}: buildPromptOptions): Promise { + const filteredMessages = messages; + + if (filteredMessages[0].from === "system" && preprompt) { + filteredMessages[0].content = preprompt; + } + + let prompt = model + .chatPromptRender({ + messages: filteredMessages, + preprompt, + tools, + toolResults, + continueMessage, + }) + // Not super precise, but it's truncated in the model's backend anyway + .split(" ") + .slice(-(model.parameters?.truncate ?? 0)) + .join(" "); + + if (continueMessage && model.parameters?.stop) { + let trimmedPrompt = prompt.trimEnd(); + let hasRemovedStop = true; + while (hasRemovedStop) { + hasRemovedStop = false; + for (const stopToken of model.parameters.stop) { + if (trimmedPrompt.endsWith(stopToken)) { + trimmedPrompt = trimmedPrompt.slice(0, -stopToken.length); + hasRemovedStop = true; + break; + } + } + trimmedPrompt = trimmedPrompt.trimEnd(); + } + prompt = trimmedPrompt; + } + + return prompt; +} diff --git a/src/lib/components/AnnouncementBanner.svelte b/src/lib/components/AnnouncementBanner.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f1b45f8081a637373fd8c31bf51d08fffd0e41ca --- /dev/null +++ b/src/lib/components/AnnouncementBanner.svelte @@ -0,0 +1,20 @@ + + +
+ New + {title} +
+ {@render children?.()} +
+
diff --git a/src/lib/components/AssistantSettings.svelte b/src/lib/components/AssistantSettings.svelte new file mode 100644 index 0000000000000000000000000000000000000000..eac3fa6e9fec2ea19be97f91d3b62885e66d8e4d --- /dev/null +++ b/src/lib/components/AssistantSettings.svelte @@ -0,0 +1,642 @@ + + +
{ + e.preventDefault(); + if (!e.target) { + return; + } + const formData = new FormData(e.target as HTMLFormElement, e.submitter); + + loading = true; + if (files?.[0] && files[0].size > 0) { + formData.set("avatar", files[0]); + } + + if (deleteExistingAvatar === true) { + if (assistant?.avatar) { + // if there is an avatar we explicitly removei t + formData.set("avatar", "null"); + } else { + // else we just remove it from the input + formData.delete("avatar"); + } + } else { + if (files === null) { + formData.delete("avatar"); + } + } + + formData.delete("ragMode"); + + if (ragMode === false || !page.data.enableAssistantsRAG) { + formData.set("ragAllowAll", "false"); + formData.set("ragLinkList", ""); + formData.set("ragDomainList", ""); + } else if (ragMode === "all") { + formData.set("ragAllowAll", "true"); + formData.set("ragLinkList", ""); + formData.set("ragDomainList", ""); + } else if (ragMode === "links") { + formData.set("ragAllowAll", "false"); + formData.set("ragDomainList", ""); + } else if (ragMode === "domains") { + formData.set("ragAllowAll", "false"); + formData.set("ragLinkList", ""); + } + + formData.set("tools", tools.join(",")); + + let response: Response; + if (assistant?._id) { + response = await fetch(`${base}/api/assistant/${assistant._id}`, { + method: "PATCH", + body: formData, + }); + if (response.ok) { + goto(`${base}/settings/assistants/${assistant?._id}`, { invalidateAll: true }); + } else { + if (response.status === 400) { + const data = await response.json(); + errors = data.errors; + } else { + $error = response.statusText; + } + } + } else { + response = await fetch(`${base}/api/assistant`, { + method: "POST", + body: formData, + }); + + if (response.ok) { + const { assistantId } = await response.json(); + goto(`${base}/settings/assistants/${assistantId}`, { invalidateAll: true }); + } else { + if (response.status === 400) { + const data = await response.json(); + errors = data.errors; + } else { + $error = response.statusText; + } + } + } + }} +> + {#if assistant} +

+ Edit Assistant: {assistant?.name ?? "assistant"} +

+

+ Modifying an existing assistant will propagate the changes to all users. +

+ {:else} +

Create new assistant

+

+ Create and share your own AI Assistant. All assistants are public +

+ {/if} + +
+
+
+
Avatar
+ + + {#if (files && files[0]) || (assistant?.avatar && !deleteExistingAvatar)} +
+ {#if files && files[0]} + avatar + {:else if assistant?.avatar} + avatar + {/if} + + +
+
+ +
+ {:else} +
+ +
+ {/if} +

{getError("avatar")}

+
+ + + + + + + + + {#if selectedModel?.tools} +
+ Tools + + Experimental + +

+ Choose up to 3 community tools that will be used with this assistant. +

+
+ + {/if} + {#if page.data.enableAssistantsRAG} +
+ Internet access + + + {#if isHuggingChat} + Give feedback + {/if} + + + + + + + + {#if ragMode === "domains"} + + Specify domains and URLs that the application can search, separated by commas. + + + +

{getError("ragDomainList")}

+ {/if} + + + {#if ragMode === "links"} + + Specify a maximum of 10 direct URLs that the Assistant will access. HTML & Plain Text + only, separated by commas + + +

{getError("ragLinkList")}

+ {/if} +
+ {/if} +
+ +
+
+ Instructions (System Prompt) + {#if dynamicPrompt && templateVariables.length} +
+ + +
+ {/if} +
+ + +
+ + {#if modelId} + {@const model = models.find((_model) => _model.id === modelId)} + {#if model?.tokenizer && systemPrompt} + + {/if} + {/if} + +

{getError("preprompt")}

+
+
+ + Cancel + + +
+
+
+
diff --git a/src/lib/components/AssistantToolPicker.svelte b/src/lib/components/AssistantToolPicker.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5661cda502c383f383357568c9c16ea04c6f4482 --- /dev/null +++ b/src/lib/components/AssistantToolPicker.svelte @@ -0,0 +1,150 @@ + + +{#if selectedValues.length > 0} +
+ {#each selectedValues as value} +
+ {#key value.color + value.icon} + + {/key} +
+ {value.displayName} + {#if value.createdByName} +

+ Created by + {value.createdByName} +

+ {:else} +

Official HuggingChat tool

+ {/if} +
+ +
+ {/each} +
+{/if} + +{#if selectedValues.length < maxValues} +
+ { + inputValue = ev.currentTarget.value; + debouncedFetch(inputValue); + }} + disabled={selectedValues.length >= maxValues} + class="w-full rounded border border-gray-200 bg-gray-100 px-3 py-2" + class:opacity-50={selectedValues.length >= maxValues} + class:bg-gray-100={selectedValues.length >= maxValues} + placeholder="Type to search tools..." + tabindex="0" + /> + {#if suggestions.length > 0} + + {/if} +
+{/if} diff --git a/src/lib/components/CodeBlock.svelte b/src/lib/components/CodeBlock.svelte new file mode 100644 index 0000000000000000000000000000000000000000..db8513825d62f5c32a0949769c9ef60cf5b042ce --- /dev/null +++ b/src/lib/components/CodeBlock.svelte @@ -0,0 +1,22 @@ + + +
+
{@html DOMPurify.sanitize(code)}
+ +
diff --git a/src/lib/components/ContinueBtn.svelte b/src/lib/components/ContinueBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f3c300971c184e252708f965ffab3eebdd1cddaa --- /dev/null +++ b/src/lib/components/ContinueBtn.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/CopyToClipBoardBtn.svelte b/src/lib/components/CopyToClipBoardBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b04953b237e869425a2dcaa10cd8921ec337bffd --- /dev/null +++ b/src/lib/components/CopyToClipBoardBtn.svelte @@ -0,0 +1,79 @@ + + + diff --git a/src/lib/components/DisclaimerModal.svelte b/src/lib/components/DisclaimerModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..830b3162817f48bc52e829e6f56dd18a5ce353b8 --- /dev/null +++ b/src/lib/components/DisclaimerModal.svelte @@ -0,0 +1,75 @@ + + + +
+

+ + {envPublic.PUBLIC_APP_NAME} +

+ +

+ {envPublic.PUBLIC_APP_DESCRIPTION} +

+ +

+ {envPublic.PUBLIC_APP_DISCLAIMER_MESSAGE} +

+ +
+ + {#if page.data.loginEnabled} +
+ +
+ {/if} +
+
+
diff --git a/src/lib/components/ExpandNavigation.svelte b/src/lib/components/ExpandNavigation.svelte new file mode 100644 index 0000000000000000000000000000000000000000..45aa691f1e00791538b9631242b1de379bcfaa1a --- /dev/null +++ b/src/lib/components/ExpandNavigation.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/HoverTooltip.svelte b/src/lib/components/HoverTooltip.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9fe990defdb422287e3cb27d9f2fa31d411e3d78 --- /dev/null +++ b/src/lib/components/HoverTooltip.svelte @@ -0,0 +1,44 @@ + + +
+ {@render children?.()} + + +
diff --git a/src/lib/components/InfiniteScroll.svelte b/src/lib/components/InfiniteScroll.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bd0ebc87b308ec97971a5a04a0d1556457ed8490 --- /dev/null +++ b/src/lib/components/InfiniteScroll.svelte @@ -0,0 +1,50 @@ + + +
+
+
+
+
diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f78188fa68fb9e1d059cf47440ac381d817077d7 --- /dev/null +++ b/src/lib/components/LoginModal.svelte @@ -0,0 +1,63 @@ + + + +
+

+ + {envPublic.PUBLIC_APP_NAME} +

+

+ {envPublic.PUBLIC_APP_DESCRIPTION} +

+

+ {envPublic.PUBLIC_APP_GUEST_MESSAGE} +

+ +
+ {#if page.data.loginRequired} + + {:else} + + {/if} +
+
+
diff --git a/src/lib/components/MobileNav.svelte b/src/lib/components/MobileNav.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ce7e5e49603031e4efb8d7ec30a2daef8d84e4e1 --- /dev/null +++ b/src/lib/components/MobileNav.svelte @@ -0,0 +1,138 @@ + + + + + diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e2bbec31af0eb6d43f51ac272eba95f74beb76d0 --- /dev/null +++ b/src/lib/components/Modal.svelte @@ -0,0 +1,82 @@ + + + + +
{ + e.stopPropagation(); + handleBackdropClick(e); + }} + transition:fade|local={{ easing: cubicOut, duration: 300 }} + class="fixed inset-0 z-40 flex items-center justify-center bg-black/80 backdrop-blur-sm dark:bg-black/50" + > + +
+
diff --git a/src/lib/components/ModelCardMetadata.svelte b/src/lib/components/ModelCardMetadata.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4cfd7593b248c5e7f6465cd8a590e3a928289b10 --- /dev/null +++ b/src/lib/components/ModelCardMetadata.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/NavConversationItem.svelte b/src/lib/components/NavConversationItem.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b129a3a7f1c26d2e128385e8534ede5dd51f93ab --- /dev/null +++ b/src/lib/components/NavConversationItem.svelte @@ -0,0 +1,113 @@ + + + { + confirmDelete = false; + }} + href="{base}/conversation/{conv.id}" + class="group flex h-10 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 sm:h-[2.35rem] {conv.id === + page.params.id + ? 'bg-gray-100 dark:bg-gray-700' + : ''}" +> +
+ {#if confirmDelete} + Delete + {/if} + {#if conv.avatarUrl} + Assistant avatar + {conv.title.replace(/\p{Emoji}/gu, "")} + {:else if conv.assistantId} +
+ {conv.title.replace(/\p{Emoji}/gu, "")} + {:else} + {conv.title} + {/if} +
+ + {#if confirmDelete} + + + {:else} + + + + {/if} +
diff --git a/src/lib/components/NavMenu.svelte b/src/lib/components/NavMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..84fe0da89b53e417e7c5063559331367313977b2 --- /dev/null +++ b/src/lib/components/NavMenu.svelte @@ -0,0 +1,216 @@ + + +
+ + + {envPublic.PUBLIC_APP_NAME} + + {#if $page.url.pathname !== base + "/"} + + New Chat + + {/if} +
+
+ {#await groupedConversations} + {#if $page.data.nConversations > 0} +
+
+
+ {#each Array(100) as _} +
+ {/each} +
+
+ {/if} + {:then groupedConversations} +
+ {#each Object.entries(groupedConversations) as [group, convs]} + {#if convs.length} +

+ {titles[group]} +

+ {#each convs as conv} + + {/each} + {/if} + {/each} +
+ {#if hasMore} + + {/if} + {/await} +
+
+ {#if user?.username || user?.email} +
+ {user?.username || user?.email} + {#if !user.logoutDisabled} + + {/if} +
+ {/if} + {#if canLogin} +
+ +
+ {/if} + + + {#if $page.data.enableAssistants} + + Assistants + + {/if} + {#if $page.data.enableCommunityTools} + + Tools + New + + {/if} + + + Settings + + {#if envPublic.PUBLIC_APP_NAME === "HuggingChat"} + + About & Privacy + + {/if} +
diff --git a/src/lib/components/OpenWebSearchResults.svelte b/src/lib/components/OpenWebSearchResults.svelte new file mode 100644 index 0000000000000000000000000000000000000000..80f534186a13da98d44c2b69ca8a99d6e29cbf6b --- /dev/null +++ b/src/lib/components/OpenWebSearchResults.svelte @@ -0,0 +1,138 @@ + + +
+ +
+ + + + +
+
+
Web Search
+
+ {#if sources} + Completed + {:else} + {"message" in lastMessage ? lastMessage.message : "An error occurred"} + {/if} +
+
+ +
+ +
+ {#if webSearchMessages.length === 0} +
+ +
+ {:else} +
    + {#each webSearchMessages as message} + {#if message.subtype === MessageWebSearchUpdateType.Update} +
  1. +
    +
    +

    + {message.message} +

    +
    + {#if message.args} +

    + {message.args} +

    + {/if} +
  2. + {:else if message.subtype === MessageWebSearchUpdateType.Error} +
  3. +
    + +

    + {message.message} +

    +
    + {#if message.args} +

    + {message.args} +

    + {/if} +
  4. + {/if} + {/each} +
+ {/if} +
+
+ + diff --git a/src/lib/components/OverloadedModal.svelte b/src/lib/components/OverloadedModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..14aa6228fc8cd4b1da957f5d80e65c90fe8e63a2 --- /dev/null +++ b/src/lib/components/OverloadedModal.svelte @@ -0,0 +1,42 @@ + + + +
+
+ +

+ This model is currently overloaded. +

+
+ +

+ Please try again later or consider using a different model. We currently have {page.data + .models.length} models available. +

+ +
+ + +
+
+
diff --git a/src/lib/components/Pagination.svelte b/src/lib/components/Pagination.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b55f4cdbf418fb108783c73d748b342432bb1592 --- /dev/null +++ b/src/lib/components/Pagination.svelte @@ -0,0 +1,97 @@ + + +{#if numTotalPages > 1} + +{/if} diff --git a/src/lib/components/PaginationArrow.svelte b/src/lib/components/PaginationArrow.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3310d2b657c6b94d7f3a45516faf228ff1bd2f24 --- /dev/null +++ b/src/lib/components/PaginationArrow.svelte @@ -0,0 +1,27 @@ + + + + {#if direction === "previous"} + + Previous + {:else} + Next + + {/if} + diff --git a/src/lib/components/Portal.svelte b/src/lib/components/Portal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..24971e607b47f58c51f360be309e37ba8fdae800 --- /dev/null +++ b/src/lib/components/Portal.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/lib/components/RetryBtn.svelte b/src/lib/components/RetryBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5abeddac0aee9c570f775537d9ebd5b55c1a1aa1 --- /dev/null +++ b/src/lib/components/RetryBtn.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/ScrollToBottomBtn.svelte b/src/lib/components/ScrollToBottomBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b897ea7e9dec8cfdbb4e17b5f96d383f0eca01b5 --- /dev/null +++ b/src/lib/components/ScrollToBottomBtn.svelte @@ -0,0 +1,47 @@ + + +{#if visible} + +{/if} diff --git a/src/lib/components/ScrollToPreviousBtn.svelte b/src/lib/components/ScrollToPreviousBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..68d65d8b14c1e028436ac5374e8e0be61d17334b --- /dev/null +++ b/src/lib/components/ScrollToPreviousBtn.svelte @@ -0,0 +1,77 @@ + + +{#if visible} + +{/if} diff --git a/src/lib/components/StopGeneratingBtn.svelte b/src/lib/components/StopGeneratingBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0b457e18502fee3a5fe0df3a292af7ebd539e2a7 --- /dev/null +++ b/src/lib/components/StopGeneratingBtn.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/Switch.svelte b/src/lib/components/Switch.svelte new file mode 100644 index 0000000000000000000000000000000000000000..df8fcfec02fd7f95bf176a0157e3dcc3b479d523 --- /dev/null +++ b/src/lib/components/Switch.svelte @@ -0,0 +1,20 @@ + + + +
+
+
diff --git a/src/lib/components/SystemPromptModal.svelte b/src/lib/components/SystemPromptModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b78c26c1a41875cb161eda2c1a2d59564739b1f8 --- /dev/null +++ b/src/lib/components/SystemPromptModal.svelte @@ -0,0 +1,40 @@ + + + + +{#if isOpen} + (isOpen = false)} width="w-full !max-w-xl"> +
+
+

System Prompt

+ +
+ +
+
+{/if} diff --git a/src/lib/components/Toast.svelte b/src/lib/components/Toast.svelte new file mode 100644 index 0000000000000000000000000000000000000000..30acb6e7292d591dfd0ee7d4e0a4b942d7a310ee --- /dev/null +++ b/src/lib/components/Toast.svelte @@ -0,0 +1,25 @@ + + + +
+
+ +

{message}

+
+
+
diff --git a/src/lib/components/TokensCounter.svelte b/src/lib/components/TokensCounter.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1c50702cca27f7e49f66cb9a238bb1c8bcddd5b2 --- /dev/null +++ b/src/lib/components/TokensCounter.svelte @@ -0,0 +1,44 @@ + + +
+

+ {nTokens}{truncate ? `/${truncate}` : ""} +

+ +
diff --git a/src/lib/components/ToolBadge.svelte b/src/lib/components/ToolBadge.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ad832ec998e84a34f31a60cba0e2413a1ca50eac --- /dev/null +++ b/src/lib/components/ToolBadge.svelte @@ -0,0 +1,41 @@ + + +
+ {#if browser} + {#await fetch(`${base}/api/tools/${toolId}`).then((res) => res.json()) then value} + {#key value.color + value.icon} + + {/key} +
+ {value.displayName} + {#if value.createdByName} +

+ Created by + {value.createdByName} +

+ {:else} +

Official HuggingChat tool

+ {/if} +
+ {/await} + {/if} +
diff --git a/src/lib/components/ToolLogo.svelte b/src/lib/components/ToolLogo.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1ab351edd954a25f5a7e0f479aacc9a7bd341496 --- /dev/null +++ b/src/lib/components/ToolLogo.svelte @@ -0,0 +1,114 @@ + + +
+ + + + + + + + + + + + + +
diff --git a/src/lib/components/ToolsMenu.svelte b/src/lib/components/ToolsMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4b2e5ecf015cbd209cfd21cdcd7ee01515d031c0 --- /dev/null +++ b/src/lib/components/ToolsMenu.svelte @@ -0,0 +1,161 @@ + + +
{ + if (detailsEl?.hasAttribute("open")) { + detailsEl.removeAttribute("open"); + } + }} +> + + + Tools + ({activeToolCount}) + +
+
+
+ Available tools + {#if isHuggingChat} + + {/if} + +
+ {#if page.data.enableCommunityTools} + + + new + + Browse community tools ({page.data.communityToolCount ?? 0}) + + {/if} + {#each tools as tool} + {@const isChecked = $settings?.tools?.includes(tool._id)} +
+ {#if tool.type === "community"} + { + e.preventDefault(); + e.stopPropagation(); + await settings.instantSet({ + tools: $settings?.tools?.filter((t) => t !== tool._id) ?? [], + }); + }} + /> + {:else} + { + e.preventDefault(); + e.stopPropagation(); + if (isChecked) { + await settings.instantSet({ + tools: ($settings?.tools ?? []).filter((t) => t !== tool._id), + }); + } else { + await settings.instantSet({ + tools: [...($settings?.tools ?? []), tool._id], + }); + } + }} + /> + {/if} + + {#if tool.type === "community"} + + + + {/if} +
+ {/each} +
+
+
+ + diff --git a/src/lib/components/Tooltip.svelte b/src/lib/components/Tooltip.svelte new file mode 100644 index 0000000000000000000000000000000000000000..af90602dd30091b1d7cc8e243d72dea8cccca30f --- /dev/null +++ b/src/lib/components/Tooltip.svelte @@ -0,0 +1,30 @@ + + +
+ + {label} +
diff --git a/src/lib/components/UploadBtn.svelte b/src/lib/components/UploadBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fe722b6d405135fbf5b8fc3e0c68b7dd8340e3a8 --- /dev/null +++ b/src/lib/components/UploadBtn.svelte @@ -0,0 +1,34 @@ + + + diff --git a/src/lib/components/WebSearchToggle.svelte b/src/lib/components/WebSearchToggle.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a9bfa56575b450b9d70d8433c5143d2eea18b316 --- /dev/null +++ b/src/lib/components/WebSearchToggle.svelte @@ -0,0 +1,33 @@ + + +
+ + +
+ +
+

+ When enabled, the model will try to complement its answer with information queried from the + web. +

+
+
+
diff --git a/src/lib/components/chat/Alternatives.svelte b/src/lib/components/chat/Alternatives.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3e5af2162ef3b7e25bd7f7ec8a9c7cf8886208b0 --- /dev/null +++ b/src/lib/components/chat/Alternatives.svelte @@ -0,0 +1,76 @@ + + +
+ + + {currentIdx + 1} / {alternatives.length} + + + {#if !loading && message.children} + + {/if} +
diff --git a/src/lib/components/chat/AssistantIntroduction.svelte b/src/lib/components/chat/AssistantIntroduction.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f84188dbcf1217a8c73421f867e0e553f0a0d91d --- /dev/null +++ b/src/lib/components/chat/AssistantIntroduction.svelte @@ -0,0 +1,194 @@ + + +
+
+
+ {#if assistant.avatar} + avatar + {:else} +
+ {assistant?.name[0]} +
+ {/if} + +
+

Assistant

+ +

{assistant.name}

+ {#if assistant.description} +

+ {assistant.description} +

+ {/if} + + {#if assistant?.tools?.length} +
+ + Has tools +
+ {/if} + {#if hasRag} +
+ + Has internet access +
+ {/if} + + {#if assistant.createdByName} +
+ Created by + + {assistant.createdByName} + + {#if assistant.userCount && assistant.userCount > 1} + · +
+ {formatUserCount(assistant.userCount)} users +
+ {/if} +
+ {/if} +
+
+ +
+
+ + Settings +
+
+ +
+ {#if assistant.exampleInputs} +
+
+
+ {#each assistant.exampleInputs as example} + + {/each} +
+
+
+ {/if} +
diff --git a/src/lib/components/chat/ChatInput.svelte b/src/lib/components/chat/ChatInput.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3f19a3d86f7a60479df9cadad470d65302bb965c --- /dev/null +++ b/src/lib/components/chat/ChatInput.svelte @@ -0,0 +1,204 @@ + + +
+ + + {#if !showNoTools} +
+ +
+ {/if} + {@render children?.()} +
+ + diff --git a/src/lib/components/chat/ChatIntroduction.svelte b/src/lib/components/chat/ChatIntroduction.svelte new file mode 100644 index 0000000000000000000000000000000000000000..510075c40baaa5c95abe93ebadd789616780ba04 --- /dev/null +++ b/src/lib/components/chat/ChatIntroduction.svelte @@ -0,0 +1,94 @@ + + +
+
+
+
+ + {envPublic.PUBLIC_APP_NAME} + +
+

+ {envPublic.PUBLIC_APP_DESCRIPTION || + "Making the community's best AI chat models available to everyone."} +

+
+
+
+ {#each announcementBanners as banner} + + {banner.linkTitle} + + {/each} +
+
+
+
Current Model
+
+ {#if currentModel.logoUrl} + + {:else} +
+ {/if} + {currentModel.displayName} +
+
+ +
+ +
+
+ {#if currentModel.promptExamples} +
+

Examples

+
+ {#each currentModel.promptExamples as example} + + {/each} +
+
{/if} +
+
diff --git a/src/lib/components/chat/ChatMessage.svelte b/src/lib/components/chat/ChatMessage.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4485d3da0571f29f610b6b583e2ac868d8c32ba3 --- /dev/null +++ b/src/lib/components/chat/ChatMessage.svelte @@ -0,0 +1,395 @@ + + +{#if message.from === "assistant"} + + {#if alternatives.length > 1 && editMsdgId === null} + + {/if} +{/if} +{#if message.from === "user"} + +{/if} + + diff --git a/src/lib/components/chat/ChatWindow.svelte b/src/lib/components/chat/ChatWindow.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1a32c3ba71d8669c2387359198547376b36ff30f --- /dev/null +++ b/src/lib/components/chat/ChatWindow.svelte @@ -0,0 +1,545 @@ + + + { + e.preventDefault(); + bubble("dragover"); + }} + ondrop={(e) => { + e.preventDefault(); + onDrag = false; + }} +/> + +
+
+
+ {#if assistant && !!messages.length} + + {#if assistant.avatar} + Avatar + {:else} +
+ {assistant.name[0]} +
+ {/if} + + {assistant.name} +
+ {:else if preprompt && preprompt != currentModel.preprompt} + + {/if} + + {#if messages.length > 0} +
+ {#each messages as message, idx (message.id)} + a.includes(message.id)) ?? []} + isAuthor={!shared} + readOnly={isReadOnly} + isLast={idx === messages.length - 1} + bind:editMsdgId + on:retry + on:vote + on:continue + on:showAlternateMsg + /> + {/each} + {#if isReadOnly} + + {/if} +
+ {:else if pending} + + {:else if !assistant} + { + if (page.data.loginRequired) { + ev.preventDefault(); + $loginModalOpen = true; + } else { + dispatch("message", ev.detail); + } + }} + /> + {:else} + { + if (page.data.loginRequired) { + ev.preventDefault(); + $loginModalOpen = true; + } else { + dispatch("message", ev.detail); + } + }} + /> + {/if} +
+ + + + +
+
+ {#if sources?.length && !loading} +
+ {#each sources as source, index} + {#await source then src} + { + files = files.filter((_, i) => i !== index); + }} + /> + {/await} + {/each} +
+ {/if} + +
+
+ {#if loading} + dispatch("stop")} /> + {:else if lastIsError} + { + if (lastMessage && lastMessage.ancestors) { + dispatch("retry", { + id: lastMessage.id, + }); + } + }} + /> + {:else if messages && lastMessage && lastMessage.interrupted && !isReadOnly} +
+ { + if (lastMessage && lastMessage.ancestors) { + dispatch("continue", { + id: lastMessage?.id, + }); + } + }} + /> +
+ {/if} +
+
{ + e.preventDefault(); + handleSubmit(); + }} + class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 dark:border-gray-600 dark:bg-gray-700 + {isReadOnly ? 'opacity-30' : ''}" + > + {#if onDrag && isFileUploadEnabled} + + {:else} +
+ {#if lastIsError} + + {:else} + + {/if} + + {#if loading} + + {:else} + + {/if} +
+ {/if} + +
+

+ Model: + {#if !assistant} + {#if models.find((m) => m.id === currentModel.id)} + {currentModel.displayName} + {:else} + + {currentModel.id} + + {/if} + {:else} + {@const model = models.find((m) => m.id === assistant?.modelId)} + {#if model} + {model?.displayName} + {:else} + + {currentModel.id} + + {/if} + {/if} + ·
Generated content may be inaccurate + or false. +

+ {#if messages.length} + + {/if} +
+
+
+
+ + diff --git a/src/lib/components/chat/FileDropzone.svelte b/src/lib/components/chat/FileDropzone.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4320fd4c8d9614d1737187407bb9930409e472c4 --- /dev/null +++ b/src/lib/components/chat/FileDropzone.svelte @@ -0,0 +1,101 @@ + + +
(onDragInner = true)} + ondragleave={() => (onDragInner = false)} + ondragover={(e) => { + e.preventDefault(); + bubble("dragover"); + }} + class="relative flex h-28 w-full max-w-4xl flex-col items-center justify-center gap-1 rounded-xl border-2 border-dotted {onDragInner + ? 'border-blue-200 !bg-blue-500/10 text-blue-600 *:pointer-events-none dark:border-blue-600 dark:bg-blue-500/20 dark:text-blue-500' + : 'bg-gray-100 text-gray-500 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-400'}" +> + +

Drop File to add to chat

+
diff --git a/src/lib/components/chat/MarkdownRenderer.svelte b/src/lib/components/chat/MarkdownRenderer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d40e9361bb0b62a3bd1f79c3c8636684848865ec --- /dev/null +++ b/src/lib/components/chat/MarkdownRenderer.svelte @@ -0,0 +1,67 @@ + + +{#each tokens as token} + {#if token.type === "text"} + {#await token.html then html} + + {@html DOMPurify.sanitize(html)} + {/await} + {:else if token.type === "code"} + + {/if} +{/each} diff --git a/src/lib/components/chat/ModelSwitch.svelte b/src/lib/components/chat/ModelSwitch.svelte new file mode 100644 index 0000000000000000000000000000000000000000..46863f470685ae5b68bd76bf948df0de69065b2c --- /dev/null +++ b/src/lib/components/chat/ModelSwitch.svelte @@ -0,0 +1,64 @@ + + +
+ + This model is no longer available. Switch to a new one to continue this conversation: + +
+ + +
+
diff --git a/src/lib/components/chat/OpenReasoningResults.svelte b/src/lib/components/chat/OpenReasoningResults.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ff96639b1c21d77cc3a3f3482f093ac25a576f3b --- /dev/null +++ b/src/lib/components/chat/OpenReasoningResults.svelte @@ -0,0 +1,78 @@ + + +
+ +
+
+ + + + {#if loading} + + + + {/if} + +
+
+
+
Reasoning
+
+ {summary} +
+
+ +
+ +
+ +
+
+ + diff --git a/src/lib/components/chat/ToolUpdate.svelte b/src/lib/components/chat/ToolUpdate.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4b52d93d16dda26dc181070289ac8c8f86913d1e --- /dev/null +++ b/src/lib/components/chat/ToolUpdate.svelte @@ -0,0 +1,182 @@ + + +{#if toolFnName && toolFnName !== "websearch"} +
+ + + +
+ + + + +
+ + + {toolError ? "Error calling" : toolDone ? "Called" : "Calling"} tool + {availableTools.find((tool) => tool.name === toolFnName)?.displayName ?? + toolFnName} + +
+ {#each tool as toolUpdate} + {#if toolUpdate.subtype === MessageToolUpdateType.Call} +
+

Parameters

+
+
+
    + {#each Object.entries(toolUpdate.call.parameters ?? {}) as [k, v]} + {#if v !== null} +
  • + {k}: + {v} +
  • + {/if} + {/each} +
+ {:else if toolUpdate.subtype === MessageToolUpdateType.Error} +
+

Error

+
+
+

{toolUpdate.message}

+ {:else if isMessageToolResultUpdate(toolUpdate) && toolUpdate.result.status === ToolResultStatus.Success && toolUpdate.result.display} +
+

Result

+
+
+
    + {#each toolUpdate.result.outputs as output} + {#each Object.entries(output) as [k, v]} + {#if v !== null} +
  • + {k}: + {v} +
  • + {/if} + {/each} + {/each} +
+ {/if} + {/each} +
+{/if} + + diff --git a/src/lib/components/chat/UploadedFile.svelte b/src/lib/components/chat/UploadedFile.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7d3a2ae6efe8db1db2fbfafde65d5a4b33270749 --- /dev/null +++ b/src/lib/components/chat/UploadedFile.svelte @@ -0,0 +1,242 @@ + + +{#if showModal && isClickable} + + (showModal = false)}> + {#if isImage(file.mime)} + {#if file.type === "hash"} + input from user + {:else} + + input from user + {/if} + {:else if isPlainText(file.mime)} +
+

{file.name}

+ {#if file.mime === "application/vnd.chatui.clipboard"} +

+ If you prefer to inject clipboard content directly in the chat, you can disable this + feature in the + settings page. +

+ {/if} + + {#if file.type === "hash"} + {#await fetch(urlNotTrailing + "/output/" + file.value).then((res) => res.text())} +
+ +
+ {:then result} +
{result}
+ {/await} + {:else} +
{atob(file.value)}
+ {/if} +
+ {/if} +
+{/if} + +
isClickable && (showModal = true)} + onkeydown={(e) => { + if (!isClickable) { + return; + } + if (e.key === "Enter" || e.key === " ") { + showModal = true; + } + }} + class:clickable={isClickable} + role="button" + tabindex="0" +> +
+ {#if isImage(file.mime)} +
+ {file.name} +
+ {:else if isAudio(file.mime)} + + {:else if isVideo(file.mime)} +
+ + +
+ {:else if isPlainText(file.mime)} +
+
+ +
+
+
+ {truncateMiddle(file.name, 28)} +
+ {#if file.mime === "application/vnd.chatui.clipboard"} +
Clipboard source
+ {:else} +
{file.mime}
+ {/if} +
+
+ {:else if file.mime === "octet-stream"} +
+
+ +
+
+
+ {truncateMiddle(file.name, 28)} +
+
File type could not be determined
+
+ + + +
+ {:else} +
+
+ +
+
+
+ {truncateMiddle(file.name, 28)} +
+
{file.mime}
+
+
+ {/if} + + {#if canClose} + + {/if} +
+
diff --git a/src/lib/components/chat/Vote.svelte b/src/lib/components/chat/Vote.svelte new file mode 100644 index 0000000000000000000000000000000000000000..444da8afc18a47378c72d523b78695b1218fbd60 --- /dev/null +++ b/src/lib/components/chat/Vote.svelte @@ -0,0 +1,40 @@ + + + + diff --git a/src/lib/components/icons/IconChevron.svelte b/src/lib/components/icons/IconChevron.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a0d17dc029483411400ebf3eeb8833fb963fd90f --- /dev/null +++ b/src/lib/components/icons/IconChevron.svelte @@ -0,0 +1,24 @@ + + + + + diff --git a/src/lib/components/icons/IconCopy.svelte b/src/lib/components/icons/IconCopy.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1676b170402de387a944dc409c71fc614a178931 --- /dev/null +++ b/src/lib/components/icons/IconCopy.svelte @@ -0,0 +1,30 @@ + + + diff --git a/src/lib/components/icons/IconDazzled.svelte b/src/lib/components/icons/IconDazzled.svelte new file mode 100644 index 0000000000000000000000000000000000000000..10599690445360ec78ac926552c7989fba24c9ee --- /dev/null +++ b/src/lib/components/icons/IconDazzled.svelte @@ -0,0 +1,40 @@ + + + + + + + + + + + + diff --git a/src/lib/components/icons/IconImageGen.svelte b/src/lib/components/icons/IconImageGen.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5c9f595bd8731cf7454721321b73951955e1bd62 --- /dev/null +++ b/src/lib/components/icons/IconImageGen.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/icons/IconInternet.svelte b/src/lib/components/icons/IconInternet.svelte new file mode 100644 index 0000000000000000000000000000000000000000..74eba8bbc1a56cb95eb98745755b2effdfc62e20 --- /dev/null +++ b/src/lib/components/icons/IconInternet.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/icons/IconLoading.svelte b/src/lib/components/icons/IconLoading.svelte new file mode 100644 index 0000000000000000000000000000000000000000..78b754b29303d3a56422146e232299bf90ae7956 --- /dev/null +++ b/src/lib/components/icons/IconLoading.svelte @@ -0,0 +1,22 @@ + + +
+
+
+
+
diff --git a/src/lib/components/icons/IconNew.svelte b/src/lib/components/icons/IconNew.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a617467c809353ba0cc86897fda4e7b4d61fd219 --- /dev/null +++ b/src/lib/components/icons/IconNew.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/icons/IconPaperclip.svelte b/src/lib/components/icons/IconPaperclip.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a5d236b7cec2939eae799a44e6016ff527e4c155 --- /dev/null +++ b/src/lib/components/icons/IconPaperclip.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/lib/components/icons/IconScreenshot.svelte b/src/lib/components/icons/IconScreenshot.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c9edf133bb45331c84041869d52b6b391880aa33 --- /dev/null +++ b/src/lib/components/icons/IconScreenshot.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/lib/components/icons/IconTool.svelte b/src/lib/components/icons/IconTool.svelte new file mode 100644 index 0000000000000000000000000000000000000000..49b35e8000c35e5dde9531c85e8a5c715fc2313b --- /dev/null +++ b/src/lib/components/icons/IconTool.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Logo.svelte b/src/lib/components/icons/Logo.svelte new file mode 100644 index 0000000000000000000000000000000000000000..220a0826dfe9adaf027b3f971d2746d9470065d3 --- /dev/null +++ b/src/lib/components/icons/Logo.svelte @@ -0,0 +1,41 @@ + + + + + + +{#if envPublic.PUBLIC_APP_ASSETS === "chatui"} + + + +{:else} + {envPublic.PUBLIC_APP_NAME} logo +{/if} diff --git a/src/lib/components/icons/LogoHuggingFaceBorderless.svelte b/src/lib/components/icons/LogoHuggingFaceBorderless.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0f1cc6062f9dbbaae49bd12ed476af3fc336665d --- /dev/null +++ b/src/lib/components/icons/LogoHuggingFaceBorderless.svelte @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + diff --git a/src/lib/components/players/AudioPlayer.svelte b/src/lib/components/players/AudioPlayer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e95baf2411f823cb57ec90c65f35d95029a63c46 --- /dev/null +++ b/src/lib/components/players/AudioPlayer.svelte @@ -0,0 +1,82 @@ + + +
+ + + +
+
{name}
+ {#if duration !== Infinity} +
+ {format(time)} +
{ + paused = true; + }} + onpointerup={seek} + > +
+
+ {duration ? format(duration) : "--:--"} +
+ {/if} +
+
diff --git a/src/lib/constants/pagination.ts b/src/lib/constants/pagination.ts new file mode 100644 index 0000000000000000000000000000000000000000..a054569f16aea5df77d71973f4881d8fdd8722ad --- /dev/null +++ b/src/lib/constants/pagination.ts @@ -0,0 +1 @@ +export const CONV_NUM_PER_PAGE = 30; diff --git a/src/lib/constants/publicSepToken.ts b/src/lib/constants/publicSepToken.ts new file mode 100644 index 0000000000000000000000000000000000000000..15d962d69ba33e1abeb8a35885aa7647d24cf7af --- /dev/null +++ b/src/lib/constants/publicSepToken.ts @@ -0,0 +1 @@ +export const PUBLIC_SEP_TOKEN = "
"; diff --git a/src/lib/jobs/refresh-assistants-counts.ts b/src/lib/jobs/refresh-assistants-counts.ts new file mode 100644 index 0000000000000000000000000000000000000000..d9c79a3d454e5b75135c63765be8f7596306955d --- /dev/null +++ b/src/lib/jobs/refresh-assistants-counts.ts @@ -0,0 +1,106 @@ +import { Database } from "$lib/server/database"; +import { acquireLock, refreshLock } from "$lib/migrations/lock"; +import type { ObjectId } from "mongodb"; +import { subDays } from "date-fns"; +import { logger } from "$lib/server/logger"; +import { collections } from "$lib/server/database"; +const LOCK_KEY = "assistants.count"; + +let hasLock = false; +let lockId: ObjectId | null = null; + +async function getLastComputationTime(): Promise { + const lastStats = await collections.assistantStats.findOne({}, { sort: { "date.at": -1 } }); + return lastStats?.date?.at || new Date(0); +} + +async function shouldComputeStats(): Promise { + const lastComputationTime = await getLastComputationTime(); + const oneDayAgo = new Date(Date.now() - 24 * 3_600_000); + return lastComputationTime < oneDayAgo; +} + +async function refreshAssistantsCountsHelper() { + if (!hasLock) { + return; + } + + try { + await (await Database.getInstance()).getClient().withSession((session) => + session.withTransaction(async () => { + await ( + await Database.getInstance() + ) + .getCollections() + .assistants.aggregate([ + { $project: { _id: 1 } }, + { $set: { last24HoursCount: 0 } }, + { + $unionWith: { + coll: "assistants.stats", + pipeline: [ + { + $match: { "date.at": { $gte: subDays(new Date(), 1) }, "date.span": "hour" }, + }, + { + $group: { + _id: "$assistantId", + last24HoursCount: { $sum: "$count" }, + }, + }, + ], + }, + }, + { + $group: { + _id: "$_id", + last24HoursCount: { $sum: "$last24HoursCount" }, + }, + }, + { + $merge: { + into: "assistants", + on: "_id", + whenMatched: "merge", + whenNotMatched: "discard", + }, + }, + ]) + .next(); + }) + ); + } catch (e) { + logger.error(e, "Refresh assistants counter failed!"); + } +} + +async function maintainLock() { + if (hasLock && lockId) { + hasLock = await refreshLock(LOCK_KEY, lockId); + + if (!hasLock) { + lockId = null; + } + } else if (!hasLock) { + lockId = (await acquireLock(LOCK_KEY)) || null; + hasLock = !!lockId; + } + + setTimeout(maintainLock, 10_000); +} + +export function refreshAssistantsCounts() { + const ONE_HOUR_MS = 3_600_000; + + maintainLock().then(async () => { + if (await shouldComputeStats()) { + refreshAssistantsCountsHelper(); + } + + setInterval(async () => { + if (await shouldComputeStats()) { + refreshAssistantsCountsHelper(); + } + }, 24 * ONE_HOUR_MS); + }); +} diff --git a/src/lib/jobs/refresh-conversation-stats.ts b/src/lib/jobs/refresh-conversation-stats.ts new file mode 100644 index 0000000000000000000000000000000000000000..539c768b1c826a0a86baae42c86852b7ada5b2b3 --- /dev/null +++ b/src/lib/jobs/refresh-conversation-stats.ts @@ -0,0 +1,271 @@ +import type { ConversationStats } from "$lib/types/ConversationStats"; +import { CONVERSATION_STATS_COLLECTION, collections } from "$lib/server/database"; +import { logger } from "$lib/server/logger"; +import type { ObjectId } from "mongodb"; +import { acquireLock, refreshLock } from "$lib/migrations/lock"; + +async function getLastComputationTime(): Promise { + const lastStats = await collections.conversationStats.findOne({}, { sort: { "date.at": -1 } }); + return lastStats?.date?.at || new Date(0); +} + +async function shouldComputeStats(): Promise { + const lastComputationTime = await getLastComputationTime(); + const oneDayAgo = new Date(Date.now() - 24 * 3_600_000); + return lastComputationTime < oneDayAgo; +} + +export async function computeAllStats() { + for (const span of ["day", "week", "month"] as const) { + computeStats({ dateField: "updatedAt", type: "conversation", span }).catch((e) => + logger.error(e) + ); + computeStats({ dateField: "createdAt", type: "conversation", span }).catch((e) => + logger.error(e) + ); + computeStats({ dateField: "createdAt", type: "message", span }).catch((e) => logger.error(e)); + } +} + +async function computeStats(params: { + dateField: ConversationStats["date"]["field"]; + span: ConversationStats["date"]["span"]; + type: ConversationStats["type"]; +}) { + const lastComputed = await collections.conversationStats.findOne( + { "date.field": params.dateField, "date.span": params.span, type: params.type }, + { sort: { "date.at": -1 } } + ); + + // If the last computed week is at the beginning of the last computed month, we need to include some days from the previous month + // In those cases we need to compute the stats from before the last month as everything is one aggregation + const minDate = lastComputed ? lastComputed.date.at : new Date(0); + + logger.info( + { minDate, dateField: params.dateField, span: params.span, type: params.type }, + "Computing conversation stats" + ); + + const dateField = params.type === "message" ? "messages." + params.dateField : params.dateField; + + const pipeline = [ + { + $match: { + [dateField]: { $gte: minDate }, + }, + }, + { + $project: { + [dateField]: 1, + sessionId: 1, + userId: 1, + }, + }, + ...(params.type === "message" + ? [ + { + $unwind: "$messages", + }, + { + $match: { + [dateField]: { $gte: minDate }, + }, + }, + ] + : []), + { + $sort: { + [dateField]: 1, + }, + }, + { + $facet: { + userId: [ + { + $match: { + userId: { $exists: true }, + }, + }, + { + $group: { + _id: { + at: { $dateTrunc: { date: `$${dateField}`, unit: params.span } }, + userId: "$userId", + }, + }, + }, + { + $group: { + _id: "$_id.at", + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + date: { + at: "$_id", + field: params.dateField, + span: params.span, + }, + distinct: "userId", + count: 1, + }, + }, + ], + sessionId: [ + { + $match: { + sessionId: { $exists: true }, + }, + }, + { + $group: { + _id: { + at: { $dateTrunc: { date: `$${dateField}`, unit: params.span } }, + sessionId: "$sessionId", + }, + }, + }, + { + $group: { + _id: "$_id.at", + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + date: { + at: "$_id", + field: params.dateField, + span: params.span, + }, + distinct: "sessionId", + count: 1, + }, + }, + ], + userOrSessionId: [ + { + $group: { + _id: { + at: { $dateTrunc: { date: `$${dateField}`, unit: params.span } }, + userOrSessionId: { $ifNull: ["$userId", "$sessionId"] }, + }, + }, + }, + { + $group: { + _id: "$_id.at", + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + date: { + at: "$_id", + field: params.dateField, + span: params.span, + }, + distinct: "userOrSessionId", + count: 1, + }, + }, + ], + _id: [ + { + $group: { + _id: { $dateTrunc: { date: `$${dateField}`, unit: params.span } }, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + date: { + at: "$_id", + field: params.dateField, + span: params.span, + }, + distinct: "_id", + count: 1, + }, + }, + ], + }, + }, + { + $project: { + stats: { + $concatArrays: ["$userId", "$sessionId", "$userOrSessionId", "$_id"], + }, + }, + }, + { + $unwind: "$stats", + }, + { + $replaceRoot: { + newRoot: "$stats", + }, + }, + { + $set: { + type: params.type, + }, + }, + { + $merge: { + into: CONVERSATION_STATS_COLLECTION, + on: ["date.at", "type", "date.span", "date.field", "distinct"], + whenMatched: "replace", + whenNotMatched: "insert", + }, + }, + ]; + + await collections.conversations.aggregate(pipeline, { allowDiskUse: true }).next(); + + logger.info( + { minDate, dateField: params.dateField, span: params.span, type: params.type }, + "Computed conversation stats" + ); +} + +const LOCK_KEY = "conversation.stats"; + +let hasLock = false; +let lockId: ObjectId | null = null; + +async function maintainLock() { + if (hasLock && lockId) { + hasLock = await refreshLock(LOCK_KEY, lockId); + + if (!hasLock) { + lockId = null; + } + } else if (!hasLock) { + lockId = (await acquireLock(LOCK_KEY)) || null; + hasLock = !!lockId; + } + + setTimeout(maintainLock, 10_000); +} + +export function refreshConversationStats() { + const ONE_HOUR_MS = 3_600_000; + + maintainLock().then(async () => { + if (await shouldComputeStats()) { + computeAllStats(); + } + + setInterval(async () => { + if (await shouldComputeStats()) { + computeAllStats(); + } + }, 24 * ONE_HOUR_MS); + }); +} diff --git a/src/lib/migrations/lock.ts b/src/lib/migrations/lock.ts new file mode 100644 index 0000000000000000000000000000000000000000..6df5d261b418f68f6f7af2ca7ca9ecccae1f54e6 --- /dev/null +++ b/src/lib/migrations/lock.ts @@ -0,0 +1,53 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; + +/** + * Returns the lock id if the lock was acquired, false otherwise + */ +export async function acquireLock(key: string): Promise { + try { + const id = new ObjectId(); + + const insert = await collections.semaphores.insertOne({ + _id: id, + key, + createdAt: new Date(), + updatedAt: new Date(), + }); + + return insert.acknowledged ? id : false; // true if the document was inserted + } catch (e) { + // unique index violation, so there must already be a lock + return false; + } +} + +export async function releaseLock(key: string, lockId: ObjectId) { + await collections.semaphores.deleteOne({ + _id: lockId, + key, + }); +} + +export async function isDBLocked(key: string): Promise { + const res = await collections.semaphores.countDocuments({ + key, + }); + return res > 0; +} + +export async function refreshLock(key: string, lockId: ObjectId): Promise { + const result = await collections.semaphores.updateOne( + { + _id: lockId, + key, + }, + { + $set: { + updatedAt: new Date(), + }, + } + ); + + return result.matchedCount > 0; +} diff --git a/src/lib/migrations/migrations.spec.ts b/src/lib/migrations/migrations.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..28c1d04678c9e7cc430d93442dc61c9fda56a620 --- /dev/null +++ b/src/lib/migrations/migrations.spec.ts @@ -0,0 +1,64 @@ +import { afterEach, assert, describe, expect, it } from "vitest"; +import { migrations } from "./routines"; +import { acquireLock, isDBLocked, refreshLock, releaseLock } from "./lock"; +import { collections } from "$lib/server/database"; + +const LOCK_KEY = "migrations.test"; + +describe( + "migrations", + { + retry: 3, + }, + () => { + it("should not have duplicates guid", async () => { + const guids = migrations.map((m) => m._id.toString()); + const uniqueGuids = [...new Set(guids)]; + expect(uniqueGuids.length).toBe(guids.length); + }); + + it("should acquire only one lock on DB", async () => { + const results = await Promise.all(new Array(1000).fill(0).map(() => acquireLock(LOCK_KEY))); + const locks = results.filter((r) => r); + + const semaphores = await collections.semaphores.find({}).toArray(); + + expect(locks.length).toBe(1); + expect(semaphores).toBeDefined(); + expect(semaphores.length).toBe(1); + expect(semaphores?.[0].key).toBe(LOCK_KEY); + }); + + it("should read the lock correctly", async () => { + const lockId = await acquireLock(LOCK_KEY); + assert(lockId); + expect(await isDBLocked(LOCK_KEY)).toBe(true); + expect(!!(await acquireLock(LOCK_KEY))).toBe(false); + await releaseLock(LOCK_KEY, lockId); + expect(await isDBLocked(LOCK_KEY)).toBe(false); + }); + + it("should refresh the lock", async () => { + const lockId = await acquireLock(LOCK_KEY); + + assert(lockId); + + // get the updatedAt time + + const updatedAtInitially = (await collections.semaphores.findOne({}))?.updatedAt; + + await refreshLock(LOCK_KEY, lockId); + + const updatedAtAfterRefresh = (await collections.semaphores.findOne({}))?.updatedAt; + + expect(updatedAtInitially).toBeDefined(); + expect(updatedAtAfterRefresh).toBeDefined(); + expect(updatedAtInitially).not.toBe(updatedAtAfterRefresh); + }); + } +); + +afterEach(async () => { + await collections.semaphores.deleteMany({}); + await collections.migrationResults.deleteMany({}); +}); diff --git a/src/lib/migrations/migrations.ts b/src/lib/migrations/migrations.ts new file mode 100644 index 0000000000000000000000000000000000000000..bfe7152ef86f9890002613164411e64d20b3f719 --- /dev/null +++ b/src/lib/migrations/migrations.ts @@ -0,0 +1,119 @@ +import { Database } from "$lib/server/database"; +import { migrations } from "./routines"; +import { acquireLock, releaseLock, isDBLocked, refreshLock } from "./lock"; +import { isHuggingChat } from "$lib/utils/isHuggingChat"; +import { logger } from "$lib/server/logger"; + +const LOCK_KEY = "migrations"; + +export async function checkAndRunMigrations() { + // make sure all GUIDs are unique + if (new Set(migrations.map((m) => m._id.toString())).size !== migrations.length) { + throw new Error("Duplicate migration GUIDs found."); + } + + // check if all migrations have already been run + const migrationResults = await (await Database.getInstance()) + .getCollections() + .migrationResults.find() + .toArray(); + + logger.info("[MIGRATIONS] Begin check..."); + + // connect to the database + const connectedClient = await (await Database.getInstance()).getClient().connect(); + + const lockId = await acquireLock(LOCK_KEY); + + if (!lockId) { + // another instance already has the lock, so we exit early + logger.info( + "[MIGRATIONS] Another instance already has the lock. Waiting for DB to be unlocked." + ); + + // Todo: is this necessary? Can we just return? + // block until the lock is released + while (await isDBLocked(LOCK_KEY)) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + return; + } + + // once here, we have the lock + // make sure to refresh it regularly while it's running + const refreshInterval = setInterval(async () => { + await refreshLock(LOCK_KEY, lockId); + }, 1000 * 10); + + // iterate over all migrations + for (const migration of migrations) { + // check if the migration has already been applied + const shouldRun = + migration.runEveryTime || + !migrationResults.find((m) => m._id.toString() === migration._id.toString()); + + // check if the migration has already been applied + if (!shouldRun) { + logger.info(`[MIGRATIONS] "${migration.name}" already applied. Skipping...`); + } else { + // check the modifiers to see if some cases match + if ( + (migration.runForHuggingChat === "only" && !isHuggingChat) || + (migration.runForHuggingChat === "never" && isHuggingChat) + ) { + logger.info( + `[MIGRATIONS] "${migration.name}" should not be applied for this run. Skipping...` + ); + continue; + } + + // otherwise all is good and we can run the migration + logger.info( + `[MIGRATIONS] "${migration.name}" ${ + migration.runEveryTime ? "should run every time" : "not applied yet" + }. Applying...` + ); + + await (await Database.getInstance()).getCollections().migrationResults.updateOne( + { _id: migration._id }, + { + $set: { + name: migration.name, + status: "ongoing", + }, + }, + { upsert: true } + ); + + const session = connectedClient.startSession(); + let result = false; + + try { + await session.withTransaction(async () => { + result = await migration.up(await Database.getInstance()); + }); + } catch (e) { + logger.info(`[MIGRATIONS] "${migration.name}" failed!`); + logger.error(e); + } finally { + await session.endSession(); + } + + await (await Database.getInstance()).getCollections().migrationResults.updateOne( + { _id: migration._id }, + { + $set: { + name: migration.name, + status: result ? "success" : "failure", + }, + }, + { upsert: true } + ); + } + } + + logger.info("[MIGRATIONS] All migrations applied. Releasing lock"); + + clearInterval(refreshInterval); + await releaseLock(LOCK_KEY, lockId); +} diff --git a/src/lib/migrations/routines/01-update-search-assistants.ts b/src/lib/migrations/routines/01-update-search-assistants.ts new file mode 100644 index 0000000000000000000000000000000000000000..52c8b2f6c99a5e9d349690271c4e28761e351b53 --- /dev/null +++ b/src/lib/migrations/routines/01-update-search-assistants.ts @@ -0,0 +1,50 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId, type AnyBulkWriteOperation } from "mongodb"; +import type { Assistant } from "$lib/types/Assistant"; +import { generateSearchTokens } from "$lib/utils/searchTokens"; + +const migration: Migration = { + _id: new ObjectId("5f9f3e3e3e3e3e3e3e3e3e3e"), + name: "Update search assistants", + up: async () => { + const { assistants } = collections; + let ops: AnyBulkWriteOperation[] = []; + + for await (const assistant of assistants + .find() + .project>({ _id: 1, name: 1 })) { + ops.push({ + updateOne: { + filter: { + _id: assistant._id, + }, + update: { + $set: { + searchTokens: generateSearchTokens(assistant.name), + }, + }, + }, + }); + + if (ops.length >= 1000) { + process.stdout.write("."); + await assistants.bulkWrite(ops, { ordered: false }); + ops = []; + } + } + + if (ops.length) { + await assistants.bulkWrite(ops, { ordered: false }); + } + + return true; + }, + down: async () => { + const { assistants } = collections; + await assistants.updateMany({}, { $unset: { searchTokens: "" } }); + return true; + }, +}; + +export default migration; diff --git a/src/lib/migrations/routines/02-update-assistants-models.ts b/src/lib/migrations/routines/02-update-assistants-models.ts new file mode 100644 index 0000000000000000000000000000000000000000..35cef0c69d9b99643bc99ae4a0f75d1ff6690ec7 --- /dev/null +++ b/src/lib/migrations/routines/02-update-assistants-models.ts @@ -0,0 +1,47 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; + +const updateAssistantsModels: Migration = { + _id: new ObjectId("5f9f3f3f3f3f3f3f3f3f3f3f"), + name: "Update deprecated models in assistants with the default model", + up: async () => { + const models = (await import("$lib/server/models")).models; + const oldModels = (await import("$lib/server/models")).oldModels; + const { assistants } = collections; + + const modelIds = models.map((el) => el.id); + const defaultModelId = models[0].id; + + // Find all assistants whose modelId is not in modelIds, and update it + const bulkOps = await assistants + .find({ modelId: { $nin: modelIds } }) + .map((assistant) => { + // has an old model + let newModelId = defaultModelId; + + const oldModel = oldModels.find((m) => m.id === assistant.modelId); + if (oldModel && oldModel.transferTo && !!models.find((m) => m.id === oldModel.transferTo)) { + newModelId = oldModel.transferTo; + } + + return { + updateOne: { + filter: { _id: assistant._id }, + update: { $set: { modelId: newModelId } }, + }, + }; + }) + .toArray(); + + if (bulkOps.length > 0) { + await assistants.bulkWrite(bulkOps); + } + + return true; + }, + runEveryTime: true, + runForHuggingChat: "only", +}; + +export default updateAssistantsModels; diff --git a/src/lib/migrations/routines/03-add-tools-in-settings.ts b/src/lib/migrations/routines/03-add-tools-in-settings.ts new file mode 100644 index 0000000000000000000000000000000000000000..0036242a1c1828f9f3822981ee6eae01a68457b8 --- /dev/null +++ b/src/lib/migrations/routines/03-add-tools-in-settings.ts @@ -0,0 +1,29 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { logger } from "$lib/server/logger"; + +const addToolsToSettings: Migration = { + _id: new ObjectId("5c9c4c4c4c4c4c4c4c4c4c4c"), + name: "Add empty 'tools' record in settings", + up: async () => { + const { settings } = collections; + + // Find all assistants whose modelId is not in modelIds, and update it to use defaultModelId + await settings.updateMany( + { + tools: { $exists: false }, + }, + { $set: { tools: [] } } + ); + + settings + .createIndex({ tools: 1 }) + .catch((e) => logger.error(e, "Error creating index during tools migration")); + + return true; + }, + runEveryTime: false, +}; + +export default addToolsToSettings; diff --git a/src/lib/migrations/routines/04-update-message-updates.ts b/src/lib/migrations/routines/04-update-message-updates.ts new file mode 100644 index 0000000000000000000000000000000000000000..8be3d91cd94f006b8efbcc9b25c6c780122aa236 --- /dev/null +++ b/src/lib/migrations/routines/04-update-message-updates.ts @@ -0,0 +1,189 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId, type WithId } from "mongodb"; +import type { Conversation } from "$lib/types/Conversation"; +import type { WebSearchSource } from "$lib/types/WebSearch"; +import { + MessageUpdateStatus, + MessageUpdateType, + MessageWebSearchUpdateType, + type MessageUpdate, + type MessageWebSearchFinishedUpdate, +} from "$lib/types/MessageUpdate"; +import type { Message } from "$lib/types/Message"; +import { isMessageWebSearchSourcesUpdate } from "$lib/utils/messageUpdates"; + +// ----------- +// Copy of the previous message update types +export type FinalAnswer = { + type: "finalAnswer"; + text: string; +}; + +export type TextStreamUpdate = { + type: "stream"; + token: string; +}; + +type WebSearchUpdate = { + type: "webSearch"; + messageType: "update" | "error" | "sources"; + message: string; + args?: string[]; + sources?: WebSearchSource[]; +}; + +type StatusUpdate = { + type: "status"; + status: "started" | "pending" | "finished" | "error" | "title"; + message?: string; +}; + +type ErrorUpdate = { + type: "error"; + message: string; + name: string; +}; + +type FileUpdate = { + type: "file"; + sha: string; +}; + +type OldMessageUpdate = + | FinalAnswer + | TextStreamUpdate + | WebSearchUpdate + | StatusUpdate + | ErrorUpdate + | FileUpdate; + +/** Converts the old message update to the new schema */ +function convertMessageUpdate(message: Message, update: OldMessageUpdate): MessageUpdate | null { + try { + // Text and files + if (update.type === "finalAnswer") { + return { + type: MessageUpdateType.FinalAnswer, + text: update.text, + interrupted: message.interrupted ?? false, + }; + } else if (update.type === "stream") { + return { + type: MessageUpdateType.Stream, + token: update.token, + }; + } else if (update.type === "file") { + return { + type: MessageUpdateType.File, + name: "Unknown", + sha: update.sha, + // assume jpeg but could be any image. should be harmless + mime: "image/jpeg", + }; + } + + // Status + else if (update.type === "status") { + if (update.status === "title") { + return { + type: MessageUpdateType.Title, + title: update.message ?? "New Chat", + }; + } + if (update.status === "pending") return null; + + const status = + update.status === "started" + ? MessageUpdateStatus.Started + : update.status === "finished" + ? MessageUpdateStatus.Finished + : MessageUpdateStatus.Error; + return { + type: MessageUpdateType.Status, + status, + message: update.message, + }; + } else if (update.type === "error") { + // Treat it as an error status update + return { + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Error, + message: update.message, + }; + } + + // Web Search + else if (update.type === "webSearch") { + if (update.messageType === "update") { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Update, + message: update.message, + args: update.args, + }; + } else if (update.messageType === "error") { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Error, + message: update.message, + args: update.args, + }; + } else if (update.messageType === "sources") { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Sources, + message: update.message, + sources: update.sources ?? [], + }; + } + } + console.warn("Unknown message update during migration:", update); + return null; + } catch (error) { + console.error("Error converting message update during migration. Skipping it... Error:", error); + return null; + } +} + +const updateMessageUpdates: Migration = { + _id: new ObjectId("5f9f7f7f7f7f7f7f7f7f7f7f"), + name: "Convert message updates to the new schema", + up: async () => { + const allConversations = collections.conversations.find({}); + + let conversation: WithId> | null = null; + while ((conversation = await allConversations.tryNext())) { + const messages = conversation.messages.map((message) => { + // Convert all of the existing updates to the new schema + const updates = message.updates + ?.map((update) => convertMessageUpdate(message, update as OldMessageUpdate)) + .filter((update): update is MessageUpdate => Boolean(update)); + + // Add the new web search finished update if the sources update exists and webSearch is defined + const webSearchSourcesUpdateIndex = updates?.findIndex(isMessageWebSearchSourcesUpdate); + if ( + message.webSearch && + updates && + webSearchSourcesUpdateIndex && + webSearchSourcesUpdateIndex !== -1 + ) { + const webSearchFinishedUpdate: MessageWebSearchFinishedUpdate = { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Finished, + }; + updates.splice(webSearchSourcesUpdateIndex + 1, 0, webSearchFinishedUpdate); + } + return { ...message, updates }; + }); + + // Set the new messages array + await collections.conversations.updateOne({ _id: conversation._id }, { $set: { messages } }); + } + + return true; + }, + runEveryTime: false, +}; + +export default updateMessageUpdates; diff --git a/src/lib/migrations/routines/05-update-message-files.ts b/src/lib/migrations/routines/05-update-message-files.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a91cb86aa399ce9b55732fbf598b1ec5cc077a4 --- /dev/null +++ b/src/lib/migrations/routines/05-update-message-files.ts @@ -0,0 +1,56 @@ +import { ObjectId, type WithId } from "mongodb"; +import { collections } from "$lib/server/database"; + +import type { Migration } from "."; +import type { Conversation } from "$lib/types/Conversation"; +import type { MessageFile } from "$lib/types/Message"; + +const updateMessageFiles: Migration = { + _id: new ObjectId("5f9f5f5f5f5f5f5f5f5f5f5f"), + name: "Convert message files to the new schema", + up: async () => { + const allConversations = collections.conversations.find({}, { projection: { messages: 1 } }); + + let conversation: WithId> | null = null; + while ((conversation = await allConversations.tryNext())) { + const messages = conversation.messages.map((message) => { + const files = (message.files as string[] | undefined)?.map((file) => { + // File is already in the new format + if (typeof file !== "string") return file; + + // File was a hash pointing to a file in the bucket + if (file.length === 64) { + return { + type: "hash", + name: "unknown.jpg", + value: file, + mime: "image/jpeg", + }; + } + // File was a base64 string + else { + return { + type: "base64", + name: "unknown.jpg", + value: file, + mime: "image/jpeg", + }; + } + }); + + return { + ...message, + files, + }; + }); + + // Set the new messages array + await collections.conversations.updateOne({ _id: conversation._id }, { $set: { messages } }); + } + + return true; + }, + runEveryTime: false, +}; + +export default updateMessageFiles; diff --git a/src/lib/migrations/routines/06-trim-message-updates.ts b/src/lib/migrations/routines/06-trim-message-updates.ts new file mode 100644 index 0000000000000000000000000000000000000000..64c080b735e3cc702d226c6ad49ab8e38a4a7ad3 --- /dev/null +++ b/src/lib/migrations/routines/06-trim-message-updates.ts @@ -0,0 +1,69 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId, type WithId } from "mongodb"; +import type { Conversation } from "$lib/types/Conversation"; +import { + MessageUpdateType, + MessageWebSearchUpdateType, + type MessageUpdate, +} from "$lib/types/MessageUpdate"; +import type { Message } from "$lib/types/Message"; +import { logger } from "$lib/server/logger"; + +// ----------- + +/** Converts the old message update to the new schema */ +function convertMessageUpdate(message: Message, update: MessageUpdate): MessageUpdate | null { + try { + // trim final websearch update, and sources update + + if (update.type === "webSearch") { + if (update.subtype === MessageWebSearchUpdateType.Sources) { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Sources, + message: update.message, + sources: update.sources.map(({ link, title }) => ({ link, title })), + }; + } else if (update.subtype === MessageWebSearchUpdateType.Finished) { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Finished, + }; + } + } + + return update; + } catch (error) { + logger.error(error, "Error converting message update during migration. Skipping it.."); + return null; + } +} + +const trimMessageUpdates: Migration = { + _id: new ObjectId("000000000000000000000006"), + name: "Trim message updates to reduce stored size", + up: async () => { + const allConversations = collections.conversations.find({}); + + let conversation: WithId> | null = null; + while ((conversation = await allConversations.tryNext())) { + const messages = conversation.messages.map((message) => { + // Convert all of the existing updates to the new schema + const updates = message.updates + ?.map((update) => convertMessageUpdate(message, update)) + .filter((update): update is MessageUpdate => Boolean(update)); + + return { ...message, updates }; + }); + + // Set the new messages array + await collections.conversations.updateOne({ _id: conversation._id }, { $set: { messages } }); + } + + return true; + }, + runEveryTime: false, +}; + +export default trimMessageUpdates; diff --git a/src/lib/migrations/routines/07-reset-tools-in-settings.ts b/src/lib/migrations/routines/07-reset-tools-in-settings.ts new file mode 100644 index 0000000000000000000000000000000000000000..03d0b89d04c8a18e4cda2df992d33a64c50af7b3 --- /dev/null +++ b/src/lib/migrations/routines/07-reset-tools-in-settings.ts @@ -0,0 +1,18 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; + +const resetTools: Migration = { + _id: new ObjectId("000000000000000000000007"), + name: "Reset tools to empty", + up: async () => { + const { settings } = collections; + + await settings.updateMany({}, { $set: { tools: [] } }); + + return true; + }, + runEveryTime: false, +}; + +export default resetTools; diff --git a/src/lib/migrations/routines/08-update-featured-to-review.ts b/src/lib/migrations/routines/08-update-featured-to-review.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ac5d8e2d8c73744afaa2a292a349a6a12796703 --- /dev/null +++ b/src/lib/migrations/routines/08-update-featured-to-review.ts @@ -0,0 +1,32 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { ReviewStatus } from "$lib/types/Review"; + +const updateFeaturedToReview: Migration = { + _id: new ObjectId("000000000000000000000008"), + name: "Update featured to review", + up: async () => { + const { assistants, tools } = collections; + + // Update assistants + await assistants.updateMany({ featured: true }, { $set: { review: ReviewStatus.APPROVED } }); + await assistants.updateMany( + { featured: { $ne: true } }, + { $set: { review: ReviewStatus.PRIVATE } } + ); + + await assistants.updateMany({}, { $unset: { featured: "" } }); + + // Update tools + await tools.updateMany({ featured: true }, { $set: { review: ReviewStatus.APPROVED } }); + await tools.updateMany({ featured: { $ne: true } }, { $set: { review: ReviewStatus.PRIVATE } }); + + await tools.updateMany({}, { $unset: { featured: "" } }); + + return true; + }, + runEveryTime: false, +}; + +export default updateFeaturedToReview; diff --git a/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts b/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0248d82d5c4fcc6a48a3cbce47c1ecff1434d7a6 --- /dev/null +++ b/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts @@ -0,0 +1,214 @@ +import type { Session } from "$lib/types/Session"; +import type { User } from "$lib/types/User"; +import type { Conversation } from "$lib/types/Conversation"; +import { ObjectId } from "mongodb"; +import { deleteConversations } from "./09-delete-empty-conversations"; +import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest"; +import { collections } from "$lib/server/database"; + +type Message = Conversation["messages"][number]; + +const userData = { + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + username: "new-username", + name: "name", + avatarUrl: "https://example.com/avatar.png", + hfUserId: "9999999999", +} satisfies User; +Object.freeze(userData); + +const sessionForUser = { + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + userId: userData._id, + sessionId: "session-id-9999999999", + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), +} satisfies Session; +Object.freeze(sessionForUser); + +const userMessage = { + from: "user", + id: "user-message-id", + content: "Hello, how are you?", +} satisfies Message; + +const assistantMessage = { + from: "assistant", + id: "assistant-message-id", + content: "I'm fine, thank you!", +} satisfies Message; + +const systemMessage = { + from: "system", + id: "system-message-id", + content: "This is a system message", +} satisfies Message; + +const conversationBase = { + _id: new ObjectId(), + createdAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + updatedAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + model: "model-id", + embeddingModel: "embedding-model-id", + title: "title", + messages: [], +} satisfies Conversation; + +describe.sequential("Deleting discarded conversations", async () => { + test("a conversation with no messages should get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(1); + }); + test("a conversation with no messages that is less than 1 hour old should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + createdAt: new Date(Date.now() - 30 * 60 * 1000), + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with only system messages should get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + messages: [systemMessage], + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(1); + }); + test("a conversation with a user message should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + messages: [userMessage], + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with an assistant message should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + messages: [assistantMessage], + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with a mix of messages should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + messages: [systemMessage, userMessage, assistantMessage, userMessage, assistantMessage], + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with a userId and no sessionId should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + messages: [userMessage, assistantMessage], + userId: userData._id, + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with no userId or sessionId should get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + messages: [userMessage, assistantMessage], + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(1); + }); + test("a conversation with a sessionId that exists should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + messages: [userMessage, assistantMessage], + sessionId: sessionForUser.sessionId, + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with a userId and a sessionId that doesn't exist should NOT get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + userId: userData._id, + messages: [userMessage, assistantMessage], + sessionId: new ObjectId().toString(), + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with only a sessionId that doesn't exist, should get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + messages: [userMessage, assistantMessage], + sessionId: new ObjectId().toString(), + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(1); + }); + test("many conversations should get deleted", async () => { + const conversations = Array.from({ length: 10010 }, () => ({ + ...conversationBase, + _id: new ObjectId(), + })); + + await collections.conversations.insertMany(conversations); + + const result = await deleteConversations(collections); + + expect(result).toBe(10010); + }); +}); + +beforeAll(async () => { + await collections.users.insertOne(userData); + await collections.sessions.insertOne(sessionForUser); +}); + +afterAll(async () => { + await collections.users.deleteOne({ + _id: userData._id, + }); + await collections.sessions.deleteOne({ + _id: sessionForUser._id, + }); + await collections.conversations.deleteMany({}); +}); + +afterEach(async () => { + await collections.conversations.deleteMany({ + _id: { $in: [conversationBase._id] }, + }); +}); diff --git a/src/lib/migrations/routines/09-delete-empty-conversations.ts b/src/lib/migrations/routines/09-delete-empty-conversations.ts new file mode 100644 index 0000000000000000000000000000000000000000..30ada9110e50bd4d5e3de0df6233b43e05492821 --- /dev/null +++ b/src/lib/migrations/routines/09-delete-empty-conversations.ts @@ -0,0 +1,88 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { Collection, FindCursor, ObjectId } from "mongodb"; +import { logger } from "$lib/server/logger"; +import type { Conversation } from "$lib/types/Conversation"; + +const BATCH_SIZE = 1000; +const DELETE_THRESHOLD_MS = 60 * 60 * 1000; + +async function deleteBatch(conversations: Collection, ids: ObjectId[]) { + if (ids.length === 0) return 0; + const deleteResult = await conversations.deleteMany({ _id: { $in: ids } }); + return deleteResult.deletedCount; +} + +async function processCursor( + cursor: FindCursor, + processBatchFn: (batch: T[]) => Promise +) { + let batch = []; + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (doc) { + batch.push(doc); + } + if (batch.length >= BATCH_SIZE) { + await processBatchFn(batch); + batch = []; + } + } + if (batch.length > 0) { + await processBatchFn(batch); + } +} + +export async function deleteConversations( + collections: typeof import("$lib/server/database").collections +) { + let deleteCount = 0; + const { conversations, sessions } = collections; + + // First criteria: Delete conversations with no user/assistant messages older than 1 hour + const emptyConvCursor = conversations + .find({ + "messages.from": { $not: { $in: ["user", "assistant"] } }, + createdAt: { $lt: new Date(Date.now() - DELETE_THRESHOLD_MS) }, + }) + .batchSize(BATCH_SIZE); + + await processCursor(emptyConvCursor, async (batch) => { + const ids = batch.map((doc) => doc._id); + deleteCount += await deleteBatch(conversations, ids); + }); + + // Second criteria: Process conversations without users in batches and check sessions + const noUserCursor = conversations.find({ userId: { $exists: false } }).batchSize(BATCH_SIZE); + + await processCursor(noUserCursor, async (batch) => { + const sessionIds = [ + ...new Set(batch.map((conv) => conv.sessionId).filter((id): id is string => !!id)), + ]; + + const existingSessions = await sessions.find({ sessionId: { $in: sessionIds } }).toArray(); + const validSessionIds = new Set(existingSessions.map((s) => s.sessionId)); + + const invalidConvs = batch.filter( + (conv) => !conv.sessionId || !validSessionIds.has(conv.sessionId) + ); + const idsToDelete = invalidConvs.map((conv) => conv._id); + deleteCount += await deleteBatch(conversations, idsToDelete); + }); + + logger.info(`[MIGRATIONS] Deleted ${deleteCount} conversations in total.`); + return deleteCount; +} + +const deleteEmptyConversations: Migration = { + _id: new ObjectId("000000000000000000000009"), + name: "Delete conversations with no user or assistant messages or valid sessions", + up: async () => { + await deleteConversations(collections); + return true; + }, + runEveryTime: false, + runForHuggingChat: "only", +}; + +export default deleteEmptyConversations; diff --git a/src/lib/migrations/routines/index.ts b/src/lib/migrations/routines/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3c509f07954dbb1564c2ae06ad8ce73db792637 --- /dev/null +++ b/src/lib/migrations/routines/index.ts @@ -0,0 +1,33 @@ +import type { ObjectId } from "mongodb"; + +import updateSearchAssistant from "./01-update-search-assistants"; +import updateAssistantsModels from "./02-update-assistants-models"; +import type { Database } from "$lib/server/database"; +import addToolsToSettings from "./03-add-tools-in-settings"; +import updateMessageUpdates from "./04-update-message-updates"; +import updateMessageFiles from "./05-update-message-files"; +import trimMessageUpdates from "./06-trim-message-updates"; +import resetTools from "./07-reset-tools-in-settings"; +import updateFeaturedToReview from "./08-update-featured-to-review"; +import deleteEmptyConversations from "./09-delete-empty-conversations"; +export interface Migration { + _id: ObjectId; + name: string; + up: (client: Database) => Promise; + down?: (client: Database) => Promise; + runForFreshInstall?: "only" | "never"; // leave unspecified to run for both + runForHuggingChat?: "only" | "never"; // leave unspecified to run for both + runEveryTime?: boolean; +} + +export const migrations: Migration[] = [ + updateSearchAssistant, + updateAssistantsModels, + addToolsToSettings, + updateMessageUpdates, + updateMessageFiles, + trimMessageUpdates, + resetTools, + updateFeaturedToReview, + deleteEmptyConversations, +]; diff --git a/src/lib/server/abortedGenerations.ts b/src/lib/server/abortedGenerations.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ff15d917d4db2ab1938c2697cac7acffbf678fb --- /dev/null +++ b/src/lib/server/abortedGenerations.ts @@ -0,0 +1,40 @@ +// Shouldn't be needed if we dove into sveltekit internals, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 + +import { logger } from "$lib/server/logger"; +import { collections } from "$lib/server/database"; +import { onExit } from "./exitHandler"; + +export class AbortedGenerations { + private static instance: AbortedGenerations; + + private abortedGenerations: Map = new Map(); + + private constructor() { + const interval = setInterval(this.updateList, 1000); + onExit(() => clearInterval(interval)); + } + + public static getInstance(): AbortedGenerations { + if (!AbortedGenerations.instance) { + AbortedGenerations.instance = new AbortedGenerations(); + } + + return AbortedGenerations.instance; + } + + public getList(): Map { + return this.abortedGenerations; + } + + private async updateList() { + try { + const aborts = await collections.abortedGenerations.find({}).sort({ createdAt: 1 }).toArray(); + + this.abortedGenerations = new Map( + aborts.map(({ conversationId, createdAt }) => [conversationId.toString(), createdAt]) + ); + } catch (err) { + logger.error(err); + } + } +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..ae170a8bc3fafd533e5b12d0e90a83846ea9b6c7 --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,177 @@ +import { + Issuer, + type BaseClient, + type UserinfoResponse, + type TokenSet, + custom, +} from "openid-client"; +import { addHours, addWeeks } from "date-fns"; +import { env } from "$env/dynamic/private"; +import { sha256 } from "$lib/utils/sha256"; +import { z } from "zod"; +import { dev } from "$app/environment"; +import type { Cookies } from "@sveltejs/kit"; +import { collections } from "$lib/server/database"; +import JSON5 from "json5"; +import { logger } from "$lib/server/logger"; + +export interface OIDCSettings { + redirectURI: string; +} + +export interface OIDCUserInfo { + token: TokenSet; + userData: UserinfoResponse; +} + +const stringWithDefault = (value: string) => + z + .string() + .default(value) + .transform((el) => (el ? el : value)); + +export const OIDConfig = z + .object({ + CLIENT_ID: stringWithDefault(env.OPENID_CLIENT_ID), + CLIENT_SECRET: stringWithDefault(env.OPENID_CLIENT_SECRET), + PROVIDER_URL: stringWithDefault(env.OPENID_PROVIDER_URL), + SCOPES: stringWithDefault(env.OPENID_SCOPES), + NAME_CLAIM: stringWithDefault(env.OPENID_NAME_CLAIM).refine( + (el) => !["preferred_username", "email", "picture", "sub"].includes(el), + { message: "nameClaim cannot be one of the restricted keys." } + ), + TOLERANCE: stringWithDefault(env.OPENID_TOLERANCE), + RESOURCE: stringWithDefault(env.OPENID_RESOURCE), + ID_TOKEN_SIGNED_RESPONSE_ALG: z.string().optional(), + }) + .parse(JSON5.parse(env.OPENID_CONFIG || "{}")); + +export const requiresUser = !!OIDConfig.CLIENT_ID && !!OIDConfig.CLIENT_SECRET; + +const sameSite = z + .enum(["lax", "none", "strict"]) + .default(dev || env.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none") + .parse(env.COOKIE_SAMESITE === "" ? undefined : env.COOKIE_SAMESITE); + +const secure = z + .boolean() + .default(!(dev || env.ALLOW_INSECURE_COOKIES === "true")) + .parse(env.COOKIE_SECURE === "" ? undefined : env.COOKIE_SECURE === "true"); + +export function refreshSessionCookie(cookies: Cookies, sessionId: string) { + cookies.set(env.COOKIE_NAME, sessionId, { + path: "/", + // So that it works inside the space's iframe + sameSite, + secure, + httpOnly: true, + expires: addWeeks(new Date(), 2), + }); +} + +export async function findUser(sessionId: string) { + const session = await collections.sessions.findOne({ sessionId }); + + if (!session) { + return null; + } + + return await collections.users.findOne({ _id: session.userId }); +} +export const authCondition = (locals: App.Locals) => { + return locals.user + ? { userId: locals.user._id } + : { sessionId: locals.sessionId, userId: { $exists: false } }; +}; + +/** + * Generates a CSRF token using the user sessionId. Note that we don't need a secret because sessionId is enough. + */ +export async function generateCsrfToken(sessionId: string, redirectUrl: string): Promise { + const data = { + expiration: addHours(new Date(), 1).getTime(), + redirectUrl, + }; + + return Buffer.from( + JSON.stringify({ + data, + signature: await sha256(JSON.stringify(data) + "##" + sessionId), + }) + ).toString("base64"); +} + +async function getOIDCClient(settings: OIDCSettings): Promise { + const issuer = await Issuer.discover(OIDConfig.PROVIDER_URL); + + const client_config: ConstructorParameters[0] = { + client_id: OIDConfig.CLIENT_ID, + client_secret: OIDConfig.CLIENT_SECRET, + redirect_uris: [settings.redirectURI], + response_types: ["code"], + [custom.clock_tolerance]: OIDConfig.TOLERANCE || undefined, + id_token_signed_response_alg: OIDConfig.ID_TOKEN_SIGNED_RESPONSE_ALG || undefined, + }; + + const alg_supported = issuer.metadata["id_token_signing_alg_values_supported"]; + + if (Array.isArray(alg_supported)) { + client_config.id_token_signed_response_alg ??= alg_supported[0]; + } + + return new issuer.Client(client_config); +} + +export async function getOIDCAuthorizationUrl( + settings: OIDCSettings, + params: { sessionId: string } +): Promise { + const client = await getOIDCClient(settings); + const csrfToken = await generateCsrfToken(params.sessionId, settings.redirectURI); + + return client.authorizationUrl({ + scope: OIDConfig.SCOPES, + state: csrfToken, + resource: OIDConfig.RESOURCE || undefined, + }); +} + +export async function getOIDCUserData( + settings: OIDCSettings, + code: string, + iss?: string +): Promise { + const client = await getOIDCClient(settings); + const token = await client.callback(settings.redirectURI, { code, iss }); + const userData = await client.userinfo(token); + + return { token, userData }; +} + +export async function validateAndParseCsrfToken( + token: string, + sessionId: string +): Promise<{ + /** This is the redirect url that was passed to the OIDC provider */ + redirectUrl: string; +} | null> { + try { + const { data, signature } = z + .object({ + data: z.object({ + expiration: z.number().int(), + redirectUrl: z.string().url(), + }), + signature: z.string().length(64), + }) + .parse(JSON.parse(token)); + const reconstructSign = await sha256(JSON.stringify(data) + "##" + sessionId); + + if (data.expiration > Date.now() && signature === reconstructSign) { + return { redirectUrl: data.redirectUrl }; + } + } catch (e) { + logger.error(e); + } + return null; +} diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c452accc76186d81e9d5d07e1bc2c2644c7abf1 --- /dev/null +++ b/src/lib/server/database.ts @@ -0,0 +1,303 @@ +import { env } from "$env/dynamic/private"; +import { GridFSBucket, MongoClient } from "mongodb"; +import type { Conversation } from "$lib/types/Conversation"; +import type { SharedConversation } from "$lib/types/SharedConversation"; +import type { AbortedGeneration } from "$lib/types/AbortedGeneration"; +import type { Settings } from "$lib/types/Settings"; +import type { User } from "$lib/types/User"; +import type { MessageEvent } from "$lib/types/MessageEvent"; +import type { Session } from "$lib/types/Session"; +import type { Assistant } from "$lib/types/Assistant"; +import type { Report } from "$lib/types/Report"; +import type { ConversationStats } from "$lib/types/ConversationStats"; +import type { MigrationResult } from "$lib/types/MigrationResult"; +import type { Semaphore } from "$lib/types/Semaphore"; +import type { AssistantStats } from "$lib/types/AssistantStats"; +import type { CommunityToolDB } from "$lib/types/Tool"; +import { MongoMemoryServer } from "mongodb-memory-server"; +import { logger } from "$lib/server/logger"; +import { building } from "$app/environment"; +import type { TokenCache } from "$lib/types/TokenCache"; +import { onExit } from "./exitHandler"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { existsSync, mkdirSync } from "fs"; + +export const CONVERSATION_STATS_COLLECTION = "conversations.stats"; + +function findRepoRoot(startPath: string): string { + let currentPath = startPath; + while (currentPath !== "/") { + if (existsSync(join(currentPath, "package.json"))) { + return currentPath; + } + currentPath = dirname(currentPath); + } + throw new Error("Could not find repository root (no package.json found)"); +} + +export class Database { + private client?: MongoClient; + private mongoServer?: MongoMemoryServer; + + private static instance: Database; + + private async init() { + if (!env.MONGODB_URL) { + logger.warn("No MongoDB URL found, using in-memory server"); + + // Find repo root by looking for package.json + const currentFilePath = fileURLToPath(import.meta.url); + const repoRoot = findRepoRoot(dirname(currentFilePath)); + + // Use MONGO_STORAGE_PATH from env if set, otherwise use db/ in repo root + const dbPath = env.MONGO_STORAGE_PATH || join(repoRoot, "db"); + + logger.info(`Using database path: ${dbPath}`); + // Create db directory if it doesn't exist + if (!existsSync(dbPath)) { + logger.info(`Creating database directory at ${dbPath}`); + mkdirSync(dbPath, { recursive: true }); + } + + this.mongoServer = await MongoMemoryServer.create({ + instance: { + dbName: env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""), + dbPath, + }, + binary: { + version: "7.0.18", + }, + }); + this.client = new MongoClient(this.mongoServer.getUri(), { + directConnection: env.MONGODB_DIRECT_CONNECTION === "true", + }); + } else { + this.client = new MongoClient(env.MONGODB_URL, { + directConnection: env.MONGODB_DIRECT_CONNECTION === "true", + }); + } + + this.client.connect().catch((err) => { + logger.error(err, "Connection error"); + process.exit(1); + }); + this.client.db(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); + this.client.on("open", () => this.initDatabase()); + + // Disconnect DB on exit + onExit(async () => { + logger.info("Closing database connection"); + await this.client?.close(true); + await this.mongoServer?.stop(); + }); + } + + public static async getInstance(): Promise { + if (!Database.instance) { + Database.instance = new Database(); + await Database.instance.init(); + } + + return Database.instance; + } + + /** + * Return mongoClient + */ + public getClient(): MongoClient { + if (!this.client) { + throw new Error("Database not initialized"); + } + + return this.client; + } + + /** + * Return map of database's collections + */ + public getCollections() { + if (!this.client) { + throw new Error("Database not initialized"); + } + + const db = this.client.db( + env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "") + ); + + const conversations = db.collection("conversations"); + const conversationStats = db.collection(CONVERSATION_STATS_COLLECTION); + const assistants = db.collection("assistants"); + const assistantStats = db.collection("assistants.stats"); + const reports = db.collection("reports"); + const sharedConversations = db.collection("sharedConversations"); + const abortedGenerations = db.collection("abortedGenerations"); + const settings = db.collection("settings"); + const users = db.collection("users"); + const sessions = db.collection("sessions"); + const messageEvents = db.collection("messageEvents"); + const bucket = new GridFSBucket(db, { bucketName: "files" }); + const migrationResults = db.collection("migrationResults"); + const semaphores = db.collection("semaphores"); + const tokenCaches = db.collection("tokens"); + const tools = db.collection("tools"); + + return { + conversations, + conversationStats, + assistants, + assistantStats, + reports, + sharedConversations, + abortedGenerations, + settings, + users, + sessions, + messageEvents, + bucket, + migrationResults, + semaphores, + tokenCaches, + tools, + }; + } + + /** + * Init database once connected: Index creation + * @private + */ + private initDatabase() { + const { + conversations, + conversationStats, + assistants, + assistantStats, + reports, + sharedConversations, + abortedGenerations, + settings, + users, + sessions, + messageEvents, + semaphores, + tokenCaches, + tools, + } = this.getCollections(); + + conversations + .createIndex( + { sessionId: 1, updatedAt: -1 }, + { partialFilterExpression: { sessionId: { $exists: true } } } + ) + .catch((e) => logger.error(e)); + conversations + .createIndex( + { userId: 1, updatedAt: -1 }, + { partialFilterExpression: { userId: { $exists: true } } } + ) + .catch((e) => logger.error(e)); + conversations + .createIndex( + { "message.id": 1, "message.ancestors": 1 }, + { partialFilterExpression: { userId: { $exists: true } } } + ) + .catch((e) => logger.error(e)); + // Not strictly necessary, could use _id, but more convenient. Also for stats + // To do stats on conversation messages + conversations + .createIndex({ "messages.createdAt": 1 }, { sparse: true }) + .catch((e) => logger.error(e)); + // Unique index for stats + conversationStats + .createIndex( + { + type: 1, + "date.field": 1, + "date.span": 1, + "date.at": 1, + distinct: 1, + }, + { unique: true } + ) + .catch((e) => logger.error(e)); + // Allow easy check of last computed stat for given type/dateField + conversationStats + .createIndex({ + type: 1, + "date.field": 1, + "date.at": 1, + }) + .catch((e) => logger.error(e)); + abortedGenerations + .createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }) + .catch((e) => logger.error(e)); + abortedGenerations + .createIndex({ conversationId: 1 }, { unique: true }) + .catch((e) => logger.error(e)); + sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch((e) => logger.error(e)); + settings + .createIndex({ sessionId: 1 }, { unique: true, sparse: true }) + .catch((e) => logger.error(e)); + settings + .createIndex({ userId: 1 }, { unique: true, sparse: true }) + .catch((e) => logger.error(e)); + settings.createIndex({ assistants: 1 }).catch((e) => logger.error(e)); + users.createIndex({ hfUserId: 1 }, { unique: true }).catch((e) => logger.error(e)); + users + .createIndex({ sessionId: 1 }, { unique: true, sparse: true }) + .catch((e) => logger.error(e)); + // No unicity because due to renames & outdated info from oauth provider, there may be the same username on different users + users.createIndex({ username: 1 }).catch((e) => logger.error(e)); + messageEvents + .createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }) + .catch((e) => logger.error(e)); + sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch((e) => logger.error(e)); + sessions.createIndex({ sessionId: 1 }, { unique: true }).catch((e) => logger.error(e)); + assistants.createIndex({ createdById: 1, userCount: -1 }).catch((e) => logger.error(e)); + assistants.createIndex({ userCount: 1 }).catch((e) => logger.error(e)); + assistants.createIndex({ review: 1, userCount: -1 }).catch((e) => logger.error(e)); + assistants.createIndex({ modelId: 1, userCount: -1 }).catch((e) => logger.error(e)); + assistants.createIndex({ searchTokens: 1 }).catch((e) => logger.error(e)); + assistants.createIndex({ last24HoursCount: 1 }).catch((e) => logger.error(e)); + assistants + .createIndex({ last24HoursUseCount: -1, useCount: -1, _id: 1 }) + .catch((e) => logger.error(e)); + assistantStats + // Order of keys is important for the queries + .createIndex({ "date.span": 1, "date.at": 1, assistantId: 1 }, { unique: true }) + .catch((e) => logger.error(e)); + reports.createIndex({ assistantId: 1 }).catch((e) => logger.error(e)); + reports.createIndex({ createdBy: 1, assistantId: 1 }).catch((e) => logger.error(e)); + + // Unique index for semaphore and migration results + semaphores.createIndex({ key: 1 }, { unique: true }).catch((e) => logger.error(e)); + semaphores + .createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }) + .catch((e) => logger.error(e)); + tokenCaches + .createIndex({ createdAt: 1 }, { expireAfterSeconds: 5 * 60 }) + .catch((e) => logger.error(e)); + tokenCaches.createIndex({ tokenHash: 1 }).catch((e) => logger.error(e)); + tools.createIndex({ createdById: 1, userCount: -1 }).catch((e) => logger.error(e)); + tools.createIndex({ userCount: 1 }).catch((e) => logger.error(e)); + tools.createIndex({ last24HoursCount: 1 }).catch((e) => logger.error(e)); + + conversations + .createIndex({ + "messages.from": 1, + createdAt: 1, + }) + .catch((e) => logger.error(e)); + + conversations + .createIndex({ + userId: 1, + sessionId: 1, + }) + .catch((e) => logger.error(e)); + } +} + +export const collections = building + ? ({} as unknown as ReturnType) + : await Database.getInstance().then((db) => db.getCollections()); diff --git a/src/lib/server/embeddingEndpoints/embeddingEndpoints.ts b/src/lib/server/embeddingEndpoints/embeddingEndpoints.ts new file mode 100644 index 0000000000000000000000000000000000000000..053e4316605e098cf3957bceff324a6284c76481 --- /dev/null +++ b/src/lib/server/embeddingEndpoints/embeddingEndpoints.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; +import { + embeddingEndpointTei, + embeddingEndpointTeiParametersSchema, +} from "./tei/embeddingEndpoints"; +import { + embeddingEndpointTransformersJS, + embeddingEndpointTransformersJSParametersSchema, +} from "./transformersjs/embeddingEndpoints"; +import { + embeddingEndpointOpenAI, + embeddingEndpointOpenAIParametersSchema, +} from "./openai/embeddingEndpoints"; +import { embeddingEndpointHfApi, embeddingEndpointHfApiSchema } from "./hfApi/embeddingHfApi"; + +// parameters passed when generating text +interface EmbeddingEndpointParameters { + inputs: string[]; +} + +export type Embedding = number[]; + +// type signature for the endpoint +export type EmbeddingEndpoint = (params: EmbeddingEndpointParameters) => Promise; + +export const embeddingEndpointSchema = z.discriminatedUnion("type", [ + embeddingEndpointTeiParametersSchema, + embeddingEndpointTransformersJSParametersSchema, + embeddingEndpointOpenAIParametersSchema, + embeddingEndpointHfApiSchema, +]); + +type EmbeddingEndpointTypeOptions = z.infer["type"]; + +// generator function that takes in type discrimantor value for defining the endpoint and return the endpoint +export type EmbeddingEndpointGenerator = ( + inputs: Extract, { type: T }> +) => EmbeddingEndpoint | Promise; + +// list of all endpoint generators +export const embeddingEndpoints: { + [Key in EmbeddingEndpointTypeOptions]: EmbeddingEndpointGenerator; +} = { + tei: embeddingEndpointTei, + transformersjs: embeddingEndpointTransformersJS, + openai: embeddingEndpointOpenAI, + hfapi: embeddingEndpointHfApi, +}; + +export default embeddingEndpoints; diff --git a/src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts b/src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..367d11ddb846c878b9ebae9e4895a23f1a17b954 --- /dev/null +++ b/src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts @@ -0,0 +1,58 @@ +import { z } from "zod"; +import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints"; +import { chunk } from "$lib/utils/chunk"; +import { env } from "$env/dynamic/private"; +import { logger } from "$lib/server/logger"; + +export const embeddingEndpointHfApiSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("hfapi"), + authorization: z + .string() + .optional() + .transform((v) => (!v && env.HF_TOKEN ? "Bearer " + env.HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header +}); + +export async function embeddingEndpointHfApi( + input: z.input +): Promise { + const { model, authorization } = embeddingEndpointHfApiSchema.parse(input); + const url = `${env.HF_API_ROOT}/${model.id}`; + + return async ({ inputs }) => { + const batchesInputs = chunk(inputs, 128); + + const batchesResults = await Promise.all( + batchesInputs.map(async (batchInputs) => { + const response = await fetch(`${url}`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + ...(authorization ? { Authorization: authorization } : {}), + }, + body: JSON.stringify({ + inputs: { + source_sentence: batchInputs[0], + sentences: batchInputs.slice(1), + }, + }), + }); + + if (!response.ok) { + logger.error(await response.text()); + logger.error(response, "Failed to get embeddings from Hugging Face API"); + return []; + } + + const embeddings: Embedding[] = await response.json(); + return embeddings; + }) + ); + + const flatAllEmbeddings = batchesResults.flat(); + + return flatAllEmbeddings; + }; +} diff --git a/src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts b/src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1725ffad1a62de34ecc03a94846be644ea3e845 --- /dev/null +++ b/src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; +import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints"; +import { chunk } from "$lib/utils/chunk"; +import { env } from "$env/dynamic/private"; + +export const embeddingEndpointOpenAIParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("openai"), + url: z.string().url().default("https://api.openai.com/v1/embeddings"), + apiKey: z.string().default(env.OPENAI_API_KEY), + defaultHeaders: z.record(z.string()).default({}), +}); + +export async function embeddingEndpointOpenAI( + input: z.input +): Promise { + const { url, model, apiKey, defaultHeaders } = + embeddingEndpointOpenAIParametersSchema.parse(input); + + const maxBatchSize = model.maxBatchSize || 100; + + return async ({ inputs }) => { + const requestURL = new URL(url); + + const batchesInputs = chunk(inputs, maxBatchSize); + + const batchesResults = await Promise.all( + batchesInputs.map(async (batchInputs) => { + const response = await fetch(requestURL, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), + ...defaultHeaders, + }, + body: JSON.stringify({ input: batchInputs, model: model.name }), + }); + + const embeddings: Embedding[] = []; + const responseObject = await response.json(); + for (const embeddingObject of responseObject.data) { + embeddings.push(embeddingObject.embedding); + } + return embeddings; + }) + ); + + const flatAllEmbeddings = batchesResults.flat(); + + return flatAllEmbeddings; + }; +} diff --git a/src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts b/src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts new file mode 100644 index 0000000000000000000000000000000000000000..c999ceba7da550ce5f7eb1ef6e263fb3142f4101 --- /dev/null +++ b/src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts @@ -0,0 +1,75 @@ +import { z } from "zod"; +import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints"; +import { chunk } from "$lib/utils/chunk"; +import { env } from "$env/dynamic/private"; +import { logger } from "$lib/server/logger"; + +export const embeddingEndpointTeiParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("tei"), + url: z.string().url(), + authorization: z + .string() + .optional() + .transform((v) => (!v && env.HF_TOKEN ? "Bearer " + env.HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header +}); + +const getModelInfoByUrl = async (url: string, authorization?: string) => { + const { origin } = new URL(url); + + const response = await fetch(`${origin}/info`, { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + ...(authorization ? { Authorization: authorization } : {}), + }, + }); + + try { + const json = await response.json(); + return { max_client_batch_size: 32, max_batch_tokens: 16384, ...json }; + } catch { + logger.debug("Could not get info from TEI embedding endpoint. Using defaults."); + return { max_client_batch_size: 32, max_batch_tokens: 16384 }; + } +}; + +export async function embeddingEndpointTei( + input: z.input +): Promise { + const { url, model, authorization } = embeddingEndpointTeiParametersSchema.parse(input); + + const { max_client_batch_size, max_batch_tokens } = await getModelInfoByUrl(url); + const maxBatchSize = Math.min( + max_client_batch_size, + Math.floor(max_batch_tokens / model.chunkCharLength) + ); + + return async ({ inputs }) => { + const { origin } = new URL(url); + + const batchesInputs = chunk(inputs, maxBatchSize); + + const batchesResults = await Promise.all( + batchesInputs.map(async (batchInputs) => { + const response = await fetch(`${origin}/embed`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + ...(authorization ? { Authorization: authorization } : {}), + }, + body: JSON.stringify({ inputs: batchInputs, normalize: true, truncate: true }), + }); + + const embeddings: Embedding[] = await response.json(); + return embeddings; + }) + ); + + const flatAllEmbeddings = batchesResults.flat(); + + return flatAllEmbeddings; + }; +} diff --git a/src/lib/server/embeddingEndpoints/transformersjs/embeddingEndpoints.ts b/src/lib/server/embeddingEndpoints/transformersjs/embeddingEndpoints.ts new file mode 100644 index 0000000000000000000000000000000000000000..802937e39164192e54b74dca9caa4c779fa32411 --- /dev/null +++ b/src/lib/server/embeddingEndpoints/transformersjs/embeddingEndpoints.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; +import type { EmbeddingEndpoint } from "../embeddingEndpoints"; +import type { Tensor, FeatureExtractionPipeline } from "@huggingface/transformers"; +import { pipeline } from "@huggingface/transformers"; + +export const embeddingEndpointTransformersJSParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("transformersjs"), +}); + +// Use the Singleton pattern to enable lazy construction of the pipeline. +class TransformersJSModelsSingleton { + static instances: Array<[string, Promise]> = []; + + static async getInstance(modelName: string): Promise { + const modelPipelineInstance = this.instances.find(([name]) => name === modelName); + + if (modelPipelineInstance) { + const [, modelPipeline] = modelPipelineInstance; + // dispose of the previous pipeline to clear memory + await (await modelPipeline).dispose(); + this.instances = this.instances.filter(([name]) => name !== modelName); + } + const newModelPipeline = pipeline("feature-extraction", modelName); + this.instances.push([modelName, newModelPipeline]); + + return newModelPipeline; + } +} + +export async function calculateEmbedding(modelName: string, inputs: string[]) { + const extractor = await TransformersJSModelsSingleton.getInstance(modelName); + const output: Tensor = await extractor(inputs, { pooling: "mean", normalize: true }); + + return output.tolist(); +} + +export function embeddingEndpointTransformersJS( + input: z.input +): EmbeddingEndpoint { + const { model } = embeddingEndpointTransformersJSParametersSchema.parse(input); + + return async ({ inputs }) => { + return calculateEmbedding(model.name, inputs); + }; +} diff --git a/src/lib/server/embeddingModels.ts b/src/lib/server/embeddingModels.ts new file mode 100644 index 0000000000000000000000000000000000000000..67ad8fe5b1edc61aa6cde738e9ee18ca477f304f --- /dev/null +++ b/src/lib/server/embeddingModels.ts @@ -0,0 +1,108 @@ +import { env } from "$env/dynamic/private"; + +import { z } from "zod"; +import { sum } from "$lib/utils/sum"; +import { + embeddingEndpoints, + embeddingEndpointSchema, + type EmbeddingEndpoint, +} from "$lib/server/embeddingEndpoints/embeddingEndpoints"; +import { embeddingEndpointTransformersJS } from "$lib/server/embeddingEndpoints/transformersjs/embeddingEndpoints"; + +import JSON5 from "json5"; + +const modelConfig = z.object({ + /** Used as an identifier in DB */ + id: z.string().optional(), + /** Used to link to the model page, and for inference */ + name: z.string().min(1), + displayName: z.string().min(1).optional(), + description: z.string().min(1).optional(), + websiteUrl: z.string().url().optional(), + modelUrl: z.string().url().optional(), + endpoints: z.array(embeddingEndpointSchema).nonempty(), + chunkCharLength: z.number().positive(), + maxBatchSize: z.number().positive().optional(), + preQuery: z.string().default(""), + prePassage: z.string().default(""), +}); + +// Default embedding model for backward compatibility +const rawEmbeddingModelJSON = + env.TEXT_EMBEDDING_MODELS || + `[ + { + "name": "Xenova/gte-small", + "chunkCharLength": 512, + "endpoints": [ + { "type": "transformersjs" } + ] + } +]`; + +const embeddingModelsRaw = z.array(modelConfig).parse(JSON5.parse(rawEmbeddingModelJSON)); + +const processEmbeddingModel = async (m: z.infer) => ({ + ...m, + id: m.id || m.name, +}); + +const addEndpoint = (m: Awaited>) => ({ + ...m, + getEndpoint: async (): Promise => { + if (!m.endpoints) { + return embeddingEndpointTransformersJS({ + type: "transformersjs", + weight: 1, + model: m, + }); + } + + const totalWeight = sum(m.endpoints.map((e) => e.weight)); + + let random = Math.random() * totalWeight; + + for (const endpoint of m.endpoints) { + if (random < endpoint.weight) { + const args = { ...endpoint, model: m }; + + switch (args.type) { + case "tei": + return embeddingEndpoints.tei(args); + case "transformersjs": + return embeddingEndpoints.transformersjs(args); + case "openai": + return embeddingEndpoints.openai(args); + case "hfapi": + return embeddingEndpoints.hfapi(args); + default: + throw new Error(`Unknown endpoint type: ${args}`); + } + } + + random -= endpoint.weight; + } + + throw new Error(`Failed to select embedding endpoint`); + }, +}); + +export const embeddingModels = await Promise.all( + embeddingModelsRaw.map((e) => processEmbeddingModel(e).then(addEndpoint)) +); + +export const defaultEmbeddingModel = embeddingModels[0]; + +const validateEmbeddingModel = (_models: EmbeddingBackendModel[], key: "id" | "name") => { + return z.enum([_models[0][key], ..._models.slice(1).map((m) => m[key])]); +}; + +export const validateEmbeddingModelById = (_models: EmbeddingBackendModel[]) => { + return validateEmbeddingModel(_models, "id"); +}; + +export const validateEmbeddingModelByName = (_models: EmbeddingBackendModel[]) => { + return validateEmbeddingModel(_models, "name"); +}; + +export type EmbeddingBackendModel = typeof defaultEmbeddingModel; diff --git a/src/lib/server/endpoints/anthropic/endpointAnthropic.ts b/src/lib/server/endpoints/anthropic/endpointAnthropic.ts new file mode 100644 index 0000000000000000000000000000000000000000..2677bdd07b6104232c2e0731809a9b31da92963a --- /dev/null +++ b/src/lib/server/endpoints/anthropic/endpointAnthropic.ts @@ -0,0 +1,224 @@ +import { z } from "zod"; +import type { Endpoint } from "../endpoints"; +import { env } from "$env/dynamic/private"; +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import { createImageProcessorOptionsValidator } from "../images"; +import { endpointMessagesToAnthropicMessages, addToolResults } from "./utils"; +import { createDocumentProcessorOptionsValidator } from "../document"; +import type { + Tool, + ToolCall, + ToolInput, + ToolInputFile, + ToolInputFixed, + ToolInputOptional, +} from "$lib/types/Tool"; +import type Anthropic from "@anthropic-ai/sdk"; +import type { MessageParam } from "@anthropic-ai/sdk/resources/messages.mjs"; +import directlyAnswer from "$lib/server/tools/directlyAnswer"; + +export const endpointAnthropicParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("anthropic"), + baseURL: z.string().url().default("https://api.anthropic.com"), + apiKey: z.string().default(env.ANTHROPIC_API_KEY ?? "sk-"), + defaultHeaders: z.record(z.string()).optional(), + defaultQuery: z.record(z.string()).optional(), + multimodal: z + .object({ + image: createImageProcessorOptionsValidator({ + supportedMimeTypes: ["image/png", "image/jpeg", "image/webp"], + preferredMimeType: "image/webp", + // The 4 / 3 compensates for the 33% increase in size when converting to base64 + maxSizeInMB: (5 / 4) * 3, + maxWidth: 4096, + maxHeight: 4096, + }), + document: createDocumentProcessorOptionsValidator({ + supportedMimeTypes: ["application/pdf"], + maxSizeInMB: 32, + }), + }) + .default({}), +}); + +export async function endpointAnthropic( + input: z.input +): Promise { + const { baseURL, apiKey, model, defaultHeaders, defaultQuery, multimodal } = + endpointAnthropicParametersSchema.parse(input); + let Anthropic; + try { + Anthropic = (await import("@anthropic-ai/sdk")).default; + } catch (e) { + throw new Error("Failed to import @anthropic-ai/sdk", { cause: e }); + } + + const anthropic = new Anthropic({ + apiKey, + baseURL, + defaultHeaders, + defaultQuery, + }); + + return async ({ + messages, + preprompt, + generateSettings, + conversationId, + tools = [], + toolResults = [], + }) => { + let system = preprompt; + if (messages?.[0]?.from === "system") { + system = messages[0].content; + } + + let tokenId = 0; + if (tools.length === 0 && toolResults.length > 0) { + const toolNames = new Set(toolResults.map((tool) => tool.call.name)); + tools = Array.from(toolNames).map((name) => ({ + name, + description: "", + inputs: [], + })) as unknown as Tool[]; + } + + const parameters = { ...model.parameters, ...generateSettings }; + + return (async function* () { + const stream = anthropic.messages.stream({ + model: model.id ?? model.name, + tools: createAnthropicTools(tools), + tool_choice: + tools.length > 0 ? { type: "auto", disable_parallel_tool_use: false } : undefined, + messages: addToolResults( + await endpointMessagesToAnthropicMessages(messages, multimodal, conversationId), + toolResults + ) as MessageParam[], + max_tokens: parameters?.max_new_tokens, + temperature: parameters?.temperature, + top_p: parameters?.top_p, + top_k: parameters?.top_k, + stop_sequences: parameters?.stop, + system, + }); + while (true) { + const result = await Promise.race([stream.emitted("text"), stream.emitted("end")]); + + if (result === undefined) { + if ("tool_use" === stream.receivedMessages[0].stop_reason) { + // this should really create a new "Assistant" message with the tool id in it. + const toolCalls: ToolCall[] = stream.receivedMessages[0].content + .filter( + (block): block is Anthropic.Messages.ContentBlock & { type: "tool_use" } => + block.type === "tool_use" + ) + .map((block) => ({ + name: block.name, + parameters: block.input as Record, + id: block.id, + })); + + yield { + token: { id: tokenId, text: "", logprob: 0, special: false, toolCalls }, + generated_text: null, + details: null, + }; + } else { + yield { + token: { + id: tokenId++, + text: "", + logprob: 0, + special: true, + }, + generated_text: await stream.finalText(), + details: null, + } satisfies TextGenerationStreamOutput; + } + + return; + } + // Text delta + yield { + token: { + id: tokenId++, + text: result as unknown as string, + special: false, + logprob: 0, + }, + generated_text: null, + details: null, + } satisfies TextGenerationStreamOutput; + } + })(); + }; +} + +function createAnthropicTools(tools: Tool[]): Anthropic.Messages.Tool[] { + return tools + .filter((tool) => tool.name !== directlyAnswer.name) + .map((tool) => { + const properties = tool.inputs.reduce( + (acc, input) => { + acc[input.name] = convertToolInputToJSONSchema(input); + return acc; + }, + {} as Record + ); + + const required = tool.inputs + .filter((input) => input.paramType === "required") + .map((input) => input.name); + + return { + name: tool.name, + description: tool.description, + input_schema: { + type: "object", + properties, + required: required.length > 0 ? required : undefined, + }, + }; + }); +} + +function convertToolInputToJSONSchema(input: ToolInput): Record { + const baseSchema: Record = {}; + if ("description" in input) { + baseSchema["description"] = input.description || ""; + } + switch (input.paramType) { + case "optional": + baseSchema["default"] = (input as ToolInputOptional).default; + break; + case "fixed": + baseSchema["const"] = (input as ToolInputFixed).value; + break; + } + + if (input.type === "file") { + baseSchema["type"] = "string"; + baseSchema["format"] = "binary"; + baseSchema["mimeTypes"] = (input as ToolInputFile).mimeTypes; + } else { + switch (input.type) { + case "str": + baseSchema["type"] = "string"; + break; + case "int": + baseSchema["type"] = "integer"; + break; + case "float": + baseSchema["type"] = "number"; + break; + case "bool": + baseSchema["type"] = "boolean"; + break; + } + } + + return baseSchema; +} diff --git a/src/lib/server/endpoints/anthropic/endpointAnthropicVertex.ts b/src/lib/server/endpoints/anthropic/endpointAnthropicVertex.ts new file mode 100644 index 0000000000000000000000000000000000000000..06ceae7463cc861e163983b9f8551b9f9495f688 --- /dev/null +++ b/src/lib/server/endpoints/anthropic/endpointAnthropicVertex.ts @@ -0,0 +1,103 @@ +import { z } from "zod"; +import type { Endpoint } from "../endpoints"; +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import { createImageProcessorOptionsValidator } from "../images"; +import { endpointMessagesToAnthropicMessages } from "./utils"; +import type { MessageParam } from "@anthropic-ai/sdk/resources/messages.mjs"; + +export const endpointAnthropicVertexParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("anthropic-vertex"), + region: z.string().default("us-central1"), + projectId: z.string(), + defaultHeaders: z.record(z.string()).optional(), + defaultQuery: z.record(z.string()).optional(), + multimodal: z + .object({ + image: createImageProcessorOptionsValidator({ + supportedMimeTypes: ["image/png", "image/jpeg", "image/webp"], + preferredMimeType: "image/webp", + // The 4 / 3 compensates for the 33% increase in size when converting to base64 + maxSizeInMB: (5 / 4) * 3, + maxWidth: 4096, + maxHeight: 4096, + }), + }) + .default({}), +}); + +export async function endpointAnthropicVertex( + input: z.input +): Promise { + const { region, projectId, model, defaultHeaders, defaultQuery, multimodal } = + endpointAnthropicVertexParametersSchema.parse(input); + let AnthropicVertex; + try { + AnthropicVertex = (await import("@anthropic-ai/vertex-sdk")).AnthropicVertex; + } catch (e) { + throw new Error("Failed to import @anthropic-ai/vertex-sdk", { cause: e }); + } + + const anthropic = new AnthropicVertex({ + baseURL: `https://${region}-aiplatform.googleapis.com/v1`, + region, + projectId, + defaultHeaders, + defaultQuery, + }); + + return async ({ messages, preprompt }) => { + let system = preprompt; + if (messages?.[0]?.from === "system") { + system = messages[0].content; + } + + let tokenId = 0; + return (async function* () { + const stream = anthropic.messages.stream({ + model: model.id ?? model.name, + messages: (await endpointMessagesToAnthropicMessages( + messages, + multimodal + )) as MessageParam[], + max_tokens: model.parameters?.max_new_tokens, + temperature: model.parameters?.temperature, + top_p: model.parameters?.top_p, + top_k: model.parameters?.top_k, + stop_sequences: model.parameters?.stop, + system, + }); + while (true) { + const result = await Promise.race([stream.emitted("text"), stream.emitted("end")]); + + // Stream end + if (result === undefined) { + yield { + token: { + id: tokenId++, + text: "", + logprob: 0, + special: true, + }, + generated_text: await stream.finalText(), + details: null, + } satisfies TextGenerationStreamOutput; + return; + } + + // Text delta + yield { + token: { + id: tokenId++, + text: result as unknown as string, + special: false, + logprob: 0, + }, + generated_text: null, + details: null, + } satisfies TextGenerationStreamOutput; + } + })(); + }; +} diff --git a/src/lib/server/endpoints/anthropic/utils.ts b/src/lib/server/endpoints/anthropic/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..e490a81ba57971dcad3d753a52d9b1b2c8f79d54 --- /dev/null +++ b/src/lib/server/endpoints/anthropic/utils.ts @@ -0,0 +1,120 @@ +import { makeImageProcessor, type ImageProcessorOptions } from "../images"; +import { makeDocumentProcessor, type FileProcessorOptions } from "../document"; +import type { EndpointMessage } from "../endpoints"; +import type { MessageFile } from "$lib/types/Message"; +import type { + BetaImageBlockParam, + BetaMessageParam, + BetaBase64PDFBlock, +} from "@anthropic-ai/sdk/resources/beta/messages/messages.mjs"; +import type { ToolResult } from "$lib/types/Tool"; +import { downloadFile } from "$lib/server/files/downloadFile"; +import type { ObjectId } from "mongodb"; + +export async function fileToImageBlock( + file: MessageFile, + opts: ImageProcessorOptions<"image/png" | "image/jpeg" | "image/webp"> +): Promise { + const processor = makeImageProcessor(opts); + + const { image, mime } = await processor(file); + + return { + type: "image", + source: { + type: "base64", + media_type: mime, + data: image.toString("base64"), + }, + }; +} + +export async function fileToDocumentBlock( + file: MessageFile, + opts: FileProcessorOptions<"application/pdf"> +): Promise { + const processor = makeDocumentProcessor(opts); + const { file: document, mime } = await processor(file); + + return { + type: "document", + source: { + type: "base64", + media_type: mime, + data: document.toString("base64"), + }, + }; +} + +type NonSystemMessage = EndpointMessage & { from: "user" | "assistant" }; +export async function endpointMessagesToAnthropicMessages( + messages: EndpointMessage[], + multimodal: { + image: ImageProcessorOptions<"image/png" | "image/jpeg" | "image/webp">; + document?: FileProcessorOptions<"application/pdf">; + }, + conversationId?: ObjectId | undefined +): Promise { + return await Promise.all( + messages + .filter((message): message is NonSystemMessage => message.from !== "system") + .map>(async (message) => { + return { + role: message.from, + content: [ + ...(message.from === "user" + ? await Promise.all( + (message.files ?? []).map(async (file) => { + if (file.type === "hash" && conversationId) { + file = await downloadFile(file.value, conversationId); + } + + if (file.mime.startsWith("image/")) { + return fileToImageBlock(file, multimodal.image); + } else if (file.mime === "application/pdf" && multimodal.document) { + return fileToDocumentBlock(file, multimodal.document); + } else { + throw new Error(`Unsupported file type: ${file.mime}`); + } + }) + ) + : []), + { type: "text", text: message.content }, + ], + }; + }) + ); +} + +export function addToolResults( + messages: BetaMessageParam[], + toolResults: ToolResult[] +): BetaMessageParam[] { + const id = crypto.randomUUID(); + if (toolResults.length === 0) { + return messages; + } + return [ + ...messages, + { + role: "assistant", + content: toolResults.map((result, index) => ({ + type: "tool_use", + id: `tool_${index}_${id}`, + name: result.call.name, + input: result.call.parameters, + })), + }, + { + role: "user", + content: toolResults.map((result, index) => ({ + type: "tool_result", + tool_use_id: `tool_${index}_${id}`, + is_error: result.status === "error", + content: JSON.stringify( + result.status === "error" ? result.message : "outputs" in result ? result.outputs : "" + ), + })), + }, + ]; +} diff --git a/src/lib/server/endpoints/aws/endpointAws.ts b/src/lib/server/endpoints/aws/endpointAws.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9e3752c2bbcc9c5f8869a814b05e98cfea1fdad --- /dev/null +++ b/src/lib/server/endpoints/aws/endpointAws.ts @@ -0,0 +1,74 @@ +import { buildPrompt } from "$lib/buildPrompt"; +import { textGenerationStream } from "@huggingface/inference"; +import { z } from "zod"; +import type { Endpoint } from "../endpoints"; + +export const endpointAwsParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("aws"), + url: z.string().url(), + accessKey: z + .string({ + description: + "An AWS Access Key ID. If not provided, the default AWS identity resolution will be used", + }) + .min(1) + .optional(), + secretKey: z + .string({ + description: + "An AWS Access Key Secret. If not provided, the default AWS identity resolution will be used", + }) + .min(1) + .optional(), + sessionToken: z.string().optional(), + service: z.union([z.literal("sagemaker"), z.literal("lambda")]).default("sagemaker"), + region: z.string().optional(), +}); + +export async function endpointAws( + input: z.input +): Promise { + let createSignedFetcher; + try { + createSignedFetcher = (await import("aws-sigv4-fetch")).createSignedFetcher; + } catch (e) { + throw new Error("Failed to import aws-sigv4-fetch"); + } + + const { url, accessKey, secretKey, sessionToken, model, region, service } = + endpointAwsParametersSchema.parse(input); + + const signedFetch = createSignedFetcher({ + service, + region, + credentials: + accessKey && secretKey + ? { accessKeyId: accessKey, secretAccessKey: secretKey, sessionToken } + : undefined, + }); + + return async ({ messages, preprompt, continueMessage, generateSettings }) => { + const prompt = await buildPrompt({ + messages, + continueMessage, + preprompt, + model, + }); + + return textGenerationStream( + { + parameters: { ...model.parameters, ...generateSettings, return_full_text: false }, + model: url, + inputs: prompt, + }, + { + use_cache: false, + fetch: signedFetch, + } + ); + }; +} + +export default endpointAws; diff --git a/src/lib/server/endpoints/aws/endpointBedrock.ts b/src/lib/server/endpoints/aws/endpointBedrock.ts new file mode 100644 index 0000000000000000000000000000000000000000..e6d848abf45bfb6d57e49b9cc284e0da91360ff9 --- /dev/null +++ b/src/lib/server/endpoints/aws/endpointBedrock.ts @@ -0,0 +1,190 @@ +import { z } from "zod"; +import type { Endpoint } from "../endpoints"; +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images"; +import type { EndpointMessage } from "../endpoints"; +import type { MessageFile } from "$lib/types/Message"; + +export const endpointBedrockParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + type: z.literal("bedrock"), + region: z.string().default("us-east-1"), + model: z.any(), + anthropicVersion: z.string().default("bedrock-2023-05-31"), + isNova: z.boolean().default(false), + multimodal: z + .object({ + image: createImageProcessorOptionsValidator({ + supportedMimeTypes: [ + "image/png", + "image/jpeg", + "image/webp", + "image/avif", + "image/tiff", + "image/gif", + ], + preferredMimeType: "image/webp", + maxSizeInMB: Infinity, + maxWidth: 4096, + maxHeight: 4096, + }), + }) + .default({}), +}); + +export async function endpointBedrock( + input: z.input +): Promise { + const { region, model, anthropicVersion, multimodal, isNova } = + endpointBedrockParametersSchema.parse(input); + + let BedrockRuntimeClient, InvokeModelWithResponseStreamCommand; + try { + ({ BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } = await import( + "@aws-sdk/client-bedrock-runtime" + )); + } catch (error) { + throw new Error("Failed to import @aws-sdk/client-bedrock-runtime. Make sure it's installed."); + } + + const client = new BedrockRuntimeClient({ + region, + }); + const imageProcessor = makeImageProcessor(multimodal.image); + + return async ({ messages, preprompt, generateSettings }) => { + let system = preprompt; + // Use the first message as the system prompt if it's of type "system" + if (messages?.[0]?.from === "system") { + system = messages[0].content; + messages = messages.slice(1); // Remove the first system message from the array + } + + const formattedMessages = await prepareMessages(messages, isNova, imageProcessor); + + let tokenId = 0; + const parameters = { ...model.parameters, ...generateSettings }; + return (async function* () { + const baseCommandParams = { + contentType: "application/json", + accept: "application/json", + modelId: model.id, + }; + + const maxTokens = parameters.max_new_tokens || 4096; + + let bodyContent; + if (isNova) { + bodyContent = { + messages: formattedMessages, + inferenceConfig: { + maxTokens, + topP: 0.1, + temperature: 1.0, + }, + system: [{ text: system }], + }; + } else { + bodyContent = { + anthropic_version: anthropicVersion, + max_tokens: maxTokens, + messages: formattedMessages, + system, + }; + } + + const command = new InvokeModelWithResponseStreamCommand({ + ...baseCommandParams, + body: Buffer.from(JSON.stringify(bodyContent), "utf-8"), + trace: "DISABLED", + }); + + const response = await client.send(command); + + let text = ""; + + for await (const item of response.body ?? []) { + const chunk = JSON.parse(new TextDecoder().decode(item.chunk?.bytes)); + if ("contentBlockDelta" in chunk || chunk.type === "content_block_delta") { + const chunkText = chunk.contentBlockDelta?.delta?.text || chunk.delta?.text || ""; + text += chunkText; + yield { + token: { + id: tokenId++, + text: chunkText, + logprob: 0, + special: false, + }, + generated_text: null, + details: null, + } satisfies TextGenerationStreamOutput; + } else if ("messageStop" in chunk || chunk.type === "message_stop") { + yield { + token: { + id: tokenId++, + text: "", + logprob: 0, + special: true, + }, + generated_text: text, + details: null, + } satisfies TextGenerationStreamOutput; + } + } + })(); + }; +} + +// Prepare the messages excluding system prompts +async function prepareMessages( + messages: EndpointMessage[], + isNova: boolean, + imageProcessor: ReturnType +) { + const formattedMessages = []; + + for (const message of messages) { + const content = []; + + if (message.files?.length) { + content.push(...(await prepareFiles(imageProcessor, isNova, message.files))); + } + if (isNova) { + content.push({ text: message.content }); + } else { + content.push({ type: "text", text: message.content }); + } + + const lastMessage = formattedMessages[formattedMessages.length - 1]; + if (lastMessage && lastMessage.role === message.from) { + // If the last message has the same role, merge the content + lastMessage.content.push(...content); + } else { + formattedMessages.push({ role: message.from, content }); + } + } + return formattedMessages; +} + +// Process files and convert them to base64 encoded strings +async function prepareFiles( + imageProcessor: ReturnType, + isNova: boolean, + files: MessageFile[] +) { + const processedFiles = await Promise.all(files.map(imageProcessor)); + + if (isNova) { + return processedFiles.map((file) => ({ + image: { + format: file.mime.substring("image/".length), + source: { bytes: file.image.toString("base64") }, + }, + })); + } else { + return processedFiles.map((file) => ({ + type: "image", + source: { type: "base64", media_type: file.mime, data: file.image.toString("base64") }, + })); + } +} diff --git a/src/lib/server/endpoints/cloudflare/endpointCloudflare.ts b/src/lib/server/endpoints/cloudflare/endpointCloudflare.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca6390b9621d01382ca608b9dac84fcb6a7c65b8 --- /dev/null +++ b/src/lib/server/endpoints/cloudflare/endpointCloudflare.ts @@ -0,0 +1,147 @@ +import { z } from "zod"; +import type { Endpoint } from "../endpoints"; +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import { env } from "$env/dynamic/private"; +import { logger } from "$lib/server/logger"; + +export const endpointCloudflareParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("cloudflare"), + accountId: z.string().default(env.CLOUDFLARE_ACCOUNT_ID), + apiToken: z.string().default(env.CLOUDFLARE_API_TOKEN), +}); + +export async function endpointCloudflare( + input: z.input +): Promise { + const { accountId, apiToken, model } = endpointCloudflareParametersSchema.parse(input); + + if (!model.id.startsWith("@")) { + model.id = "@hf/" + model.id; + } + + const apiURL = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${model.id}`; + + return async ({ messages, preprompt, generateSettings }) => { + let messagesFormatted = messages.map((message) => ({ + role: message.from, + content: message.content, + })); + + if (messagesFormatted?.[0]?.role !== "system") { + messagesFormatted = [{ role: "system", content: preprompt ?? "" }, ...messagesFormatted]; + } + + const parameters = { ...model.parameters, ...generateSettings }; + + const payload = JSON.stringify({ + messages: messagesFormatted, + stream: true, + max_tokens: parameters?.max_new_tokens, + temperature: parameters?.temperature, + top_p: parameters?.top_p, + top_k: parameters?.top_k, + repetition_penalty: parameters?.repetition_penalty, + }); + + const res = await fetch(apiURL, { + method: "POST", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", + }, + body: payload, + }); + + if (!res.ok) { + throw new Error(`Failed to generate text: ${await res.text()}`); + } + + const encoder = new TextDecoderStream(); + const reader = res.body?.pipeThrough(encoder).getReader(); + + return (async function* () { + let stop = false; + let generatedText = ""; + let tokenId = 0; + let accumulatedData = ""; // Buffer to accumulate data chunks + + while (!stop) { + const out = await reader?.read(); + + // If it's done, we cancel + if (out?.done) { + reader?.cancel(); + return; + } + + if (!out?.value) { + return; + } + + // Accumulate the data chunk + accumulatedData += out.value; + + // Process each complete JSON object in the accumulated data + while (accumulatedData.includes("\n")) { + // Assuming each JSON object ends with a newline + const endIndex = accumulatedData.indexOf("\n"); + let jsonString = accumulatedData.substring(0, endIndex).trim(); + + // Remove the processed part from the buffer + accumulatedData = accumulatedData.substring(endIndex + 1); + + if (jsonString.startsWith("data: ")) { + jsonString = jsonString.slice(6); + let data = null; + + if (jsonString === "[DONE]") { + stop = true; + + yield { + token: { + id: tokenId++, + text: "", + logprob: 0, + special: true, + }, + generated_text: generatedText, + details: null, + } satisfies TextGenerationStreamOutput; + reader?.cancel(); + + continue; + } + + try { + data = JSON.parse(jsonString); + } catch (e) { + logger.error(e, "Failed to parse JSON"); + logger.error(jsonString, "Problematic JSON string:"); + continue; // Skip this iteration and try the next chunk + } + + // Handle the parsed data + if (data.response) { + generatedText += data.response ?? ""; + const output: TextGenerationStreamOutput = { + token: { + id: tokenId++, + text: data.response ?? "", + logprob: 0, + special: false, + }, + generated_text: null, + details: null, + }; + yield output; + } + } + } + } + })(); + }; +} + +export default endpointCloudflare; diff --git a/src/lib/server/endpoints/cohere/endpointCohere.ts b/src/lib/server/endpoints/cohere/endpointCohere.ts new file mode 100644 index 0000000000000000000000000000000000000000..c257f40422daf68dee4c52b8e608f0917e2ab9f7 --- /dev/null +++ b/src/lib/server/endpoints/cohere/endpointCohere.ts @@ -0,0 +1,186 @@ +import { z } from "zod"; +import { env } from "$env/dynamic/private"; +import type { Endpoint } from "../endpoints"; +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import type { Cohere, CohereClient } from "cohere-ai"; +import { buildPrompt } from "$lib/buildPrompt"; +import { ToolResultStatus, type ToolCall } from "$lib/types/Tool"; +import { pipeline, Writable, type Readable } from "node:stream"; +import { toolHasName } from "$lib/utils/tools"; + +export const endpointCohereParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("cohere"), + apiKey: z.string().default(env.COHERE_API_TOKEN), + clientName: z.string().optional(), + raw: z.boolean().default(false), + forceSingleStep: z.boolean().default(true), +}); + +export async function endpointCohere( + input: z.input +): Promise { + const { apiKey, clientName, model, raw, forceSingleStep } = + endpointCohereParametersSchema.parse(input); + + let cohere: CohereClient; + + try { + cohere = new (await import("cohere-ai")).CohereClient({ + token: apiKey, + clientName, + }); + } catch (e) { + throw new Error("Failed to import cohere-ai", { cause: e }); + } + + return async ({ messages, preprompt, generateSettings, continueMessage, tools, toolResults }) => { + let system = preprompt; + if (messages?.[0]?.from === "system") { + system = messages[0].content; + } + + // Tools must use [A-z_] for their names and directly_answer is banned + // It's safe to convert the tool names because we treat - and _ the same + tools = tools + ?.filter((tool) => !toolHasName("directly_answer", tool)) + .map((tool) => ({ ...tool, name: tool.name.replaceAll("-", "_") })); + + const parameters = { ...model.parameters, ...generateSettings }; + + return (async function* () { + let stream; + let tokenId = 0; + + if (raw) { + const prompt = await buildPrompt({ + messages, + model, + preprompt: system, + continueMessage, + tools, + toolResults, + }); + + stream = await cohere.chatStream({ + forceSingleStep, + message: prompt, + rawPrompting: true, + model: model.id ?? model.name, + p: parameters?.top_p, + k: parameters?.top_k, + maxTokens: parameters?.max_new_tokens, + temperature: parameters?.temperature, + stopSequences: parameters?.stop, + frequencyPenalty: parameters?.frequency_penalty, + }); + } else { + const formattedMessages = messages + .filter((message) => message.from !== "system") + .map((message) => ({ + role: message.from === "user" ? "USER" : "CHATBOT", + message: message.content, + })) satisfies Cohere.Message[]; + + stream = await cohere + .chatStream({ + forceSingleStep, + model: model.id ?? model.name, + chatHistory: formattedMessages.slice(0, -1), + message: formattedMessages[formattedMessages.length - 1].message, + preamble: system, + p: parameters?.top_p, + k: parameters?.top_k, + maxTokens: parameters?.max_new_tokens, + temperature: parameters?.temperature, + stopSequences: parameters?.stop, + frequencyPenalty: parameters?.frequency_penalty, + tools, + toolResults: + toolResults?.length && toolResults?.length > 0 + ? toolResults?.map((toolResult) => { + if (toolResult.status === ToolResultStatus.Error) { + return { call: toolResult.call, outputs: [{ error: toolResult.message }] }; + } + return { call: toolResult.call, outputs: toolResult.outputs }; + }) + : undefined, + }) + .catch(async (err) => { + if (!err.body) throw err; + + // Decode the error message and throw + const message = await convertStreamToBuffer(err.body).catch(() => { + throw err; + }); + throw Error(message, { cause: err }); + }); + } + + for await (const output of stream) { + if (output.eventType === "text-generation") { + yield { + token: { + id: tokenId++, + text: output.text, + logprob: 0, + special: false, + }, + generated_text: null, + details: null, + } satisfies TextGenerationStreamOutput; + } else if (output.eventType === "tool-calls-generation") { + yield { + token: { + id: tokenId++, + text: "", + logprob: 0, + special: true, + toolCalls: output.toolCalls as ToolCall[], + }, + generated_text: null, + details: null, + }; + } else if (output.eventType === "stream-end") { + if (["ERROR", "ERROR_TOXIC", "ERROR_LIMIT"].includes(output.finishReason)) { + throw new Error(output.finishReason); + } + yield { + token: { + id: tokenId++, + text: "", + logprob: 0, + special: true, + }, + generated_text: output.response.text, + details: null, + }; + } + } + })(); + }; +} + +async function convertStreamToBuffer(webReadableStream: Readable) { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + + pipeline( + webReadableStream, + new Writable({ + write(chunk, _, callback) { + chunks.push(chunk); + callback(); + }, + }), + (err) => { + if (err) { + reject(err); + } else { + resolve(Buffer.concat(chunks).toString("utf-8")); + } + } + ); + }); +} diff --git a/src/lib/server/endpoints/document.ts b/src/lib/server/endpoints/document.ts new file mode 100644 index 0000000000000000000000000000000000000000..70df7c09279b4db96433e6be594f4f22ca2ab5d9 --- /dev/null +++ b/src/lib/server/endpoints/document.ts @@ -0,0 +1,74 @@ +import type { MessageFile } from "$lib/types/Message"; +import { z } from "zod"; + +export interface FileProcessorOptions { + supportedMimeTypes: TMimeType[]; + maxSizeInMB: number; +} + +export type ImageProcessor = (file: MessageFile) => Promise<{ + file: Buffer; + mime: TMimeType; +}>; + +export const createDocumentProcessorOptionsValidator = ( + defaults: FileProcessorOptions +) => { + return z + .object({ + supportedMimeTypes: z + .array( + z.enum([ + defaults.supportedMimeTypes[0], + ...defaults.supportedMimeTypes.slice(1), + ]) + ) + .default(defaults.supportedMimeTypes), + maxSizeInMB: z.number().positive().default(defaults.maxSizeInMB), + }) + .default(defaults); +}; + +export type DocumentProcessor = (file: MessageFile) => { + file: Buffer; + mime: TMimeType; +}; + +export type AsyncDocumentProcessor = ( + file: MessageFile +) => Promise<{ + file: Buffer; + mime: TMimeType; +}>; + +export function makeDocumentProcessor( + options: FileProcessorOptions +): AsyncDocumentProcessor { + return async (file) => { + const { supportedMimeTypes, maxSizeInMB } = options; + const { mime, value } = file; + + const buffer = Buffer.from(value, "base64"); + const tooLargeInBytes = buffer.byteLength > maxSizeInMB * 1000 * 1000; + + if (tooLargeInBytes) { + throw Error("Document is too large"); + } + + const outputMime = validateMimeType(supportedMimeTypes, mime); + return { file: buffer, mime: outputMime }; + }; +} + +const validateMimeType = ( + supportedMimes: T, + mime: string +): T[number] => { + if (!supportedMimes.includes(mime)) { + const supportedMimesStr = supportedMimes.join(", "); + + throw Error(`Mimetype "${mime}" not found in supported mimes: ${supportedMimesStr}`); + } + + return mime; +}; diff --git a/src/lib/server/endpoints/endpoints.ts b/src/lib/server/endpoints/endpoints.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed975841b812f5d85fbb88e941e5b859a931369a --- /dev/null +++ b/src/lib/server/endpoints/endpoints.ts @@ -0,0 +1,95 @@ +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import type { TextGenerationStreamOutput, TextGenerationStreamToken } from "@huggingface/inference"; +import { endpointTgi, endpointTgiParametersSchema } from "./tgi/endpointTgi"; +import { z } from "zod"; +import endpointAws, { endpointAwsParametersSchema } from "./aws/endpointAws"; +import { endpointOAIParametersSchema, endpointOai } from "./openai/endpointOai"; +import endpointLlamacpp, { endpointLlamacppParametersSchema } from "./llamacpp/endpointLlamacpp"; +import endpointOllama, { endpointOllamaParametersSchema } from "./ollama/endpointOllama"; +import endpointVertex, { endpointVertexParametersSchema } from "./google/endpointVertex"; +import endpointGenAI, { endpointGenAIParametersSchema } from "./google/endpointGenAI"; +import { endpointBedrock, endpointBedrockParametersSchema } from "./aws/endpointBedrock"; + +import { + endpointAnthropic, + endpointAnthropicParametersSchema, +} from "./anthropic/endpointAnthropic"; +import { + endpointAnthropicVertex, + endpointAnthropicVertexParametersSchema, +} from "./anthropic/endpointAnthropicVertex"; +import type { Model } from "$lib/types/Model"; +import endpointCloudflare, { + endpointCloudflareParametersSchema, +} from "./cloudflare/endpointCloudflare"; +import { endpointCohere, endpointCohereParametersSchema } from "./cohere/endpointCohere"; +import endpointLangserve, { + endpointLangserveParametersSchema, +} from "./langserve/endpointLangserve"; + +import type { Tool, ToolCall, ToolResult } from "$lib/types/Tool"; +import type { ObjectId } from "mongodb"; + +export type EndpointMessage = Omit; + +// parameters passed when generating text +export interface EndpointParameters { + messages: EndpointMessage[]; + preprompt?: Conversation["preprompt"]; + continueMessage?: boolean; // used to signal that the last message will be extended + generateSettings?: Partial; + tools?: Tool[]; + toolResults?: ToolResult[]; + isMultimodal?: boolean; + conversationId?: ObjectId; +} + +interface CommonEndpoint { + weight: number; +} +export type TextGenerationStreamOutputWithToolsAndWebSources = TextGenerationStreamOutput & { + token: TextGenerationStreamToken & { toolCalls?: ToolCall[] }; + webSources?: { uri: string; title: string }[]; +}; +// type signature for the endpoint +export type Endpoint = ( + params: EndpointParameters +) => Promise>; + +// generator function that takes in parameters for defining the endpoint and return the endpoint +export type EndpointGenerator = (parameters: T) => Endpoint; + +// list of all endpoint generators +export const endpoints = { + tgi: endpointTgi, + anthropic: endpointAnthropic, + anthropicvertex: endpointAnthropicVertex, + bedrock: endpointBedrock, + aws: endpointAws, + openai: endpointOai, + llamacpp: endpointLlamacpp, + ollama: endpointOllama, + vertex: endpointVertex, + genai: endpointGenAI, + cloudflare: endpointCloudflare, + cohere: endpointCohere, + langserve: endpointLangserve, +}; + +export const endpointSchema = z.discriminatedUnion("type", [ + endpointAnthropicParametersSchema, + endpointAnthropicVertexParametersSchema, + endpointAwsParametersSchema, + endpointBedrockParametersSchema, + endpointOAIParametersSchema, + endpointTgiParametersSchema, + endpointLlamacppParametersSchema, + endpointOllamaParametersSchema, + endpointVertexParametersSchema, + endpointGenAIParametersSchema, + endpointCloudflareParametersSchema, + endpointCohereParametersSchema, + endpointLangserveParametersSchema, +]); +export default endpoints; diff --git a/src/lib/server/endpoints/google/endpointGenAI.ts b/src/lib/server/endpoints/google/endpointGenAI.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a8540810444e50230084a5c8fc61fa7a1f9978a --- /dev/null +++ b/src/lib/server/endpoints/google/endpointGenAI.ts @@ -0,0 +1,161 @@ +import { GoogleGenerativeAI, HarmBlockThreshold, HarmCategory } from "@google/generative-ai"; +import type { Content, Part, SafetySetting, TextPart } from "@google/generative-ai"; +import { z } from "zod"; +import type { Message, MessageFile } from "$lib/types/Message"; +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import type { Endpoint } from "../endpoints"; +import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images"; +import type { ImageProcessorOptions } from "../images"; +import { env } from "$env/dynamic/private"; + +export const endpointGenAIParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("genai"), + apiKey: z.string().default(env.GOOGLE_GENAI_API_KEY), + safetyThreshold: z + .enum([ + HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED, + HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, + HarmBlockThreshold.BLOCK_NONE, + HarmBlockThreshold.BLOCK_ONLY_HIGH, + ]) + .optional(), + multimodal: z + .object({ + image: createImageProcessorOptionsValidator({ + supportedMimeTypes: ["image/png", "image/jpeg", "image/webp"], + preferredMimeType: "image/webp", + // The 4 / 3 compensates for the 33% increase in size when converting to base64 + maxSizeInMB: (5 / 4) * 3, + maxWidth: 4096, + maxHeight: 4096, + }), + }) + .default({}), +}); + +export function endpointGenAI(input: z.input): Endpoint { + const { model, apiKey, safetyThreshold, multimodal } = endpointGenAIParametersSchema.parse(input); + + const genAI = new GoogleGenerativeAI(apiKey); + + const safetySettings = safetyThreshold + ? Object.keys(HarmCategory) + .filter((cat) => cat !== HarmCategory.HARM_CATEGORY_UNSPECIFIED) + .reduce((acc, val) => { + acc.push({ + category: val as HarmCategory, + threshold: safetyThreshold, + }); + return acc; + }, [] as SafetySetting[]) + : undefined; + + return async ({ messages, preprompt, generateSettings }) => { + const parameters = { ...model.parameters, ...generateSettings }; + + const generativeModel = genAI.getGenerativeModel({ + model: model.id ?? model.name, + safetySettings, + generationConfig: { + maxOutputTokens: parameters?.max_new_tokens ?? 4096, + stopSequences: parameters?.stop, + temperature: parameters?.temperature ?? 1, + }, + }); + + let systemMessage = preprompt; + if (messages[0].from === "system") { + systemMessage = messages[0].content; + messages.shift(); + } + + const genAIMessages = await Promise.all( + messages.map(async ({ from, content, files }: Omit): Promise => { + return { + role: from === "user" ? "user" : "model", + parts: [ + ...(await Promise.all( + (files ?? []).map((file) => fileToImageBlock(file, multimodal.image)) + )), + { text: content }, + ], + }; + }) + ); + + const result = await generativeModel.generateContentStream({ + contents: genAIMessages, + systemInstruction: + systemMessage && systemMessage.trim() !== "" + ? { + role: "system", + parts: [{ text: systemMessage }], + } + : undefined, + }); + + let tokenId = 0; + return (async function* () { + let generatedText = ""; + + for await (const data of result.stream) { + if (!data?.candidates?.length) break; // Handle case where no candidates are present + + const candidate = data.candidates[0]; + if (!candidate.content?.parts?.length) continue; // Skip if no parts are present + + const firstPart = candidate.content.parts.find((part) => "text" in part) as + | TextPart + | undefined; + if (!firstPart) continue; // Skip if no text part is found + + const content = firstPart.text; + generatedText += content; + + const output: TextGenerationStreamOutput = { + token: { + id: tokenId++, + text: content, + logprob: 0, + special: false, + }, + generated_text: null, + details: null, + }; + yield output; + } + + const output: TextGenerationStreamOutput = { + token: { + id: tokenId++, + text: "", + logprob: 0, + special: true, + }, + generated_text: generatedText, + details: null, + }; + yield output; + })(); + }; +} + +async function fileToImageBlock( + file: MessageFile, + opts: ImageProcessorOptions<"image/png" | "image/jpeg" | "image/webp"> +): Promise { + const processor = makeImageProcessor(opts); + const { image, mime } = await processor(file); + + return { + inlineData: { + mimeType: mime, + data: image.toString("base64"), + }, + }; +} + +export default endpointGenAI; diff --git a/src/lib/server/endpoints/google/endpointVertex.ts b/src/lib/server/endpoints/google/endpointVertex.ts new file mode 100644 index 0000000000000000000000000000000000000000..882e61654b2c0b67b55d9251fdcfd22dee8ae7fb --- /dev/null +++ b/src/lib/server/endpoints/google/endpointVertex.ts @@ -0,0 +1,227 @@ +import { + VertexAI, + HarmCategory, + HarmBlockThreshold, + type Content, + type TextPart, +} from "@google-cloud/vertexai"; +import type { Endpoint, TextGenerationStreamOutputWithToolsAndWebSources } from "../endpoints"; +import { z } from "zod"; +import type { Message } from "$lib/types/Message"; +import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images"; +import { createDocumentProcessorOptionsValidator, makeDocumentProcessor } from "../document"; + +export const endpointVertexParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), // allow optional and validate against emptiness + type: z.literal("vertex"), + location: z.string().default("europe-west1"), + extraBody: z.object({ model_version: z.string() }).optional(), + project: z.string(), + apiEndpoint: z.string().optional(), + safetyThreshold: z + .enum([ + HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED, + HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, + HarmBlockThreshold.BLOCK_NONE, + HarmBlockThreshold.BLOCK_ONLY_HIGH, + ]) + .optional(), + tools: z.array(z.any()).optional(), + multimodal: z + .object({ + image: createImageProcessorOptionsValidator({ + supportedMimeTypes: [ + "image/png", + "image/jpeg", + "image/webp", + "image/avif", + "image/tiff", + "image/gif", + ], + preferredMimeType: "image/webp", + maxSizeInMB: 20, + maxWidth: 4096, + maxHeight: 4096, + }), + document: createDocumentProcessorOptionsValidator({ + supportedMimeTypes: ["application/pdf", "text/plain"], + maxSizeInMB: 20, + }), + }) + .default({}), +}); + +export function endpointVertex(input: z.input): Endpoint { + const { project, location, model, apiEndpoint, safetyThreshold, tools, multimodal, extraBody } = + endpointVertexParametersSchema.parse(input); + + const vertex_ai = new VertexAI({ + project, + location, + apiEndpoint, + }); + + return async ({ messages, preprompt, generateSettings }) => { + const parameters = { ...model.parameters, ...generateSettings }; + + const hasFiles = messages.some((message) => message.files && message.files.length > 0); + + const generativeModel = vertex_ai.getGenerativeModel({ + model: extraBody?.model_version ?? model.id ?? model.name, + safetySettings: safetyThreshold + ? [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: safetyThreshold, + }, + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: safetyThreshold, + }, + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: safetyThreshold, + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: safetyThreshold, + }, + { + category: HarmCategory.HARM_CATEGORY_UNSPECIFIED, + threshold: safetyThreshold, + }, + ] + : undefined, + generationConfig: { + maxOutputTokens: parameters?.max_new_tokens ?? 4096, + stopSequences: parameters?.stop, + temperature: parameters?.temperature ?? 1, + }, + // tools and multimodal are mutually exclusive + tools: !hasFiles ? tools : undefined, + }); + + // Preprompt is the same as the first system message. + let systemMessage = preprompt; + if (messages[0].from === "system") { + systemMessage = messages[0].content; + messages.shift(); + } + + const vertexMessages = await Promise.all( + messages.map(async ({ from, content, files }: Omit): Promise => { + const imageProcessor = makeImageProcessor(multimodal.image); + const documentProcessor = makeDocumentProcessor(multimodal.document); + + const processedFilesWithNull = + files && files.length > 0 + ? await Promise.all( + files.map(async (file) => { + if (file.mime.includes("image")) { + const { image, mime } = await imageProcessor(file); + + return { file: image, mime }; + } else if (file.mime === "application/pdf" || file.mime === "text/plain") { + return documentProcessor(file); + } + + return null; + }) + ) + : []; + + const processedFiles = processedFilesWithNull.filter((file) => file !== null); + + return { + role: from === "user" ? "user" : "model", + parts: [ + ...processedFiles.map((processedFile) => ({ + inlineData: { + data: processedFile.file.toString("base64"), + mimeType: processedFile.mime, + }, + })), + { + text: content, + }, + ], + }; + }) + ); + + const result = await generativeModel.generateContentStream({ + contents: vertexMessages, + systemInstruction: systemMessage + ? { + role: "system", + parts: [ + { + text: systemMessage, + }, + ], + } + : undefined, + }); + + let tokenId = 0; + return (async function* () { + let generatedText = ""; + + const webSources = []; + + for await (const data of result.stream) { + if (!data?.candidates?.length) break; // Handle case where no candidates are present + + const candidate = data.candidates[0]; + if (!candidate.content?.parts?.length) continue; // Skip if no parts are present + + const firstPart = candidate.content.parts.find((part) => "text" in part) as + | TextPart + | undefined; + if (!firstPart) continue; // Skip if no text part is found + + const isLastChunk = !!candidate.finishReason; + + const candidateWebSources = candidate.groundingMetadata?.groundingChunks + ?.map((chunk) => { + const uri = chunk.web?.uri ?? chunk.retrievedContext?.uri; + const title = chunk.web?.title ?? chunk.retrievedContext?.title; + + if (!uri || !title) { + return null; + } + + return { + uri, + title, + }; + }) + .filter((source) => source !== null); + + if (candidateWebSources) { + webSources.push(...candidateWebSources); + } + + const content = firstPart.text; + generatedText += content; + const output: TextGenerationStreamOutputWithToolsAndWebSources = { + token: { + id: tokenId++, + text: content, + logprob: 0, + special: isLastChunk, + }, + generated_text: isLastChunk ? generatedText : null, + details: null, + webSources, + }; + yield output; + + if (isLastChunk) break; + } + })(); + }; +} +export default endpointVertex; diff --git a/src/lib/server/endpoints/images.ts b/src/lib/server/endpoints/images.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d408814cf229ad64a0128a537167a747737f84c --- /dev/null +++ b/src/lib/server/endpoints/images.ts @@ -0,0 +1,211 @@ +import type { Sharp } from "sharp"; +import sharp from "sharp"; +import type { MessageFile } from "$lib/types/Message"; +import { z, type util } from "zod"; + +export interface ImageProcessorOptions { + supportedMimeTypes: TMimeType[]; + preferredMimeType: TMimeType; + maxSizeInMB: number; + maxWidth: number; + maxHeight: number; +} +export type ImageProcessor = (file: MessageFile) => Promise<{ + image: Buffer; + mime: TMimeType; +}>; + +export function createImageProcessorOptionsValidator( + defaults: ImageProcessorOptions +) { + return z + .object({ + supportedMimeTypes: z + .array( + z.enum([ + defaults.supportedMimeTypes[0], + ...defaults.supportedMimeTypes.slice(1), + ]) + ) + .default(defaults.supportedMimeTypes), + preferredMimeType: z + .enum([defaults.supportedMimeTypes[0], ...defaults.supportedMimeTypes.slice(1)]) + .default(defaults.preferredMimeType as util.noUndefined), + maxSizeInMB: z.number().positive().default(defaults.maxSizeInMB), + maxWidth: z.number().int().positive().default(defaults.maxWidth), + maxHeight: z.number().int().positive().default(defaults.maxHeight), + }) + .default(defaults); +} + +export function makeImageProcessor( + options: ImageProcessorOptions +): ImageProcessor { + return async (file) => { + const { supportedMimeTypes, preferredMimeType, maxSizeInMB, maxWidth, maxHeight } = options; + const { mime, value } = file; + + const buffer = Buffer.from(value, "base64"); + let sharpInst = sharp(buffer); + + const metadata = await sharpInst.metadata(); + if (!metadata) throw Error("Failed to read image metadata"); + const { width, height } = metadata; + if (width === undefined || height === undefined) throw Error("Failed to read image size"); + + const tooLargeInSize = width > maxWidth || height > maxHeight; + const tooLargeInBytes = buffer.byteLength > maxSizeInMB * 1000 * 1000; + + const outputMime = chooseMimeType(supportedMimeTypes, preferredMimeType, mime, { + preferSizeReduction: tooLargeInBytes, + }); + + // Resize if necessary + if (tooLargeInSize || tooLargeInBytes) { + const size = chooseImageSize({ + mime: outputMime, + width, + height, + maxWidth, + maxHeight, + maxSizeInMB, + }); + if (size.width !== width || size.height !== height) { + sharpInst = resizeImage(sharpInst, size.width, size.height); + } + } + + // Convert format if necessary + // We always want to convert the image when the file was too large in bytes + // so we can guarantee that ideal options are used, which are expected when + // choosing the image size + if (outputMime !== mime || tooLargeInBytes) { + sharpInst = convertImage(sharpInst, outputMime); + } + + const processedImage = await sharpInst.toBuffer(); + return { image: processedImage, mime: outputMime }; + }; +} + +const outputFormats = ["png", "jpeg", "webp", "avif", "tiff", "gif"] as const; +type OutputImgFormat = (typeof outputFormats)[number]; +const isOutputFormat = (format: string): format is (typeof outputFormats)[number] => + outputFormats.includes(format as OutputImgFormat); + +export function convertImage(sharpInst: Sharp, outputMime: string): Sharp { + const [type, format] = outputMime.split("/"); + if (type !== "image") throw Error(`Requested non-image mime type: ${outputMime}`); + if (!isOutputFormat(format)) { + throw Error(`Requested to convert to an unsupported format: ${format}`); + } + + return sharpInst[format](); +} + +// heic/heif requires proprietary license +// TODO: blocking heif may be incorrect considering it also supports av1, so we should instead +// detect the compression method used via sharp().metadata().compression +// TODO: consider what to do about animated formats: apng, gif, animated webp, ... +const blocklistedMimes = ["image/heic", "image/heif"]; + +/** Sorted from largest to smallest */ +const mimesBySizeDesc = [ + "image/png", + "image/tiff", + "image/gif", + "image/jpeg", + "image/webp", + "image/avif", +]; + +/** + * Defaults to preferred format or uses existing mime if supported + * When preferSizeReduction is true, it will choose the smallest format that is supported + **/ +function chooseMimeType( + supportedMimes: T, + preferredMime: string, + mime: string, + { preferSizeReduction }: { preferSizeReduction: boolean } +): T[number] { + if (!supportedMimes.includes(preferredMime)) { + const supportedMimesStr = supportedMimes.join(", "); + throw Error( + `Preferred format "${preferredMime}" not found in supported mimes: ${supportedMimesStr}` + ); + } + + const [type] = mime.split("/"); + if (type !== "image") throw Error(`Received non-image mime type: ${mime}`); + + if (supportedMimes.includes(mime) && !preferSizeReduction) return mime; + + if (blocklistedMimes.includes(mime)) throw Error(`Received blocklisted mime type: ${mime}`); + + const smallestMime = mimesBySizeDesc.findLast((m) => supportedMimes.includes(m)); + return smallestMime ?? preferredMime; +} + +interface ImageSizeOptions { + mime: string; + width: number; + height: number; + maxWidth: number; + maxHeight: number; + maxSizeInMB: number; +} + +/** Resizes the image to fit within the specified size in MB by guessing the output size */ +export function chooseImageSize({ + mime, + width, + height, + maxWidth, + maxHeight, + maxSizeInMB, +}: ImageSizeOptions): { width: number; height: number } { + const biggestDiscrepency = Math.max(1, width / maxWidth, height / maxHeight); + + let selectedWidth = Math.ceil(width / biggestDiscrepency); + let selectedHeight = Math.ceil(height / biggestDiscrepency); + + do { + const estimatedSize = estimateImageSizeInBytes(mime, selectedWidth, selectedHeight); + if (estimatedSize < maxSizeInMB * 1024 * 1024) { + return { width: selectedWidth, height: selectedHeight }; + } + selectedWidth = Math.floor(selectedWidth / 1.1); + selectedHeight = Math.floor(selectedHeight / 1.1); + } while (selectedWidth > 1 && selectedHeight > 1); + + throw Error(`Failed to resize image to fit within ${maxSizeInMB}MB`); +} + +const mimeToCompressionRatio: Record = { + "image/png": 1 / 2, + "image/jpeg": 1 / 10, + "image/webp": 1 / 4, + "image/avif": 1 / 5, + "image/tiff": 1, + "image/gif": 1 / 5, +}; + +/** + * Guesses the side of an image in MB based on its format and dimensions + * Should guess the worst case + **/ +function estimateImageSizeInBytes(mime: string, width: number, height: number): number { + const compressionRatio = mimeToCompressionRatio[mime]; + if (!compressionRatio) throw Error(`Unsupported image format: ${mime}`); + + const bitsPerPixel = 32; // Assuming 32-bit color depth for 8-bit R G B A + const bytesPerPixel = bitsPerPixel / 8; + const uncompressedSize = width * height * bytesPerPixel; + + return uncompressedSize * compressionRatio; +} + +export function resizeImage(sharpInst: Sharp, maxWidth: number, maxHeight: number): Sharp { + return sharpInst.resize({ width: maxWidth, height: maxHeight, fit: "inside" }); +} diff --git a/src/lib/server/endpoints/langserve/endpointLangserve.ts b/src/lib/server/endpoints/langserve/endpointLangserve.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6152cf64b7420fc3b74f64eea9ce257e37928bf --- /dev/null +++ b/src/lib/server/endpoints/langserve/endpointLangserve.ts @@ -0,0 +1,129 @@ +import { buildPrompt } from "$lib/buildPrompt"; +import { z } from "zod"; +import type { Endpoint } from "../endpoints"; +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import { logger } from "$lib/server/logger"; + +export const endpointLangserveParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("langserve"), + url: z.string().url(), +}); + +export function endpointLangserve( + input: z.input +): Endpoint { + const { url, model } = endpointLangserveParametersSchema.parse(input); + + return async ({ messages, preprompt, continueMessage }) => { + const prompt = await buildPrompt({ + messages, + continueMessage, + preprompt, + model, + }); + + const r = await fetch(`${url}/stream`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + input: { text: prompt }, + }), + }); + + if (!r.ok) { + throw new Error(`Failed to generate text: ${await r.text()}`); + } + + const encoder = new TextDecoderStream(); + const reader = r.body?.pipeThrough(encoder).getReader(); + + return (async function* () { + let stop = false; + let generatedText = ""; + let tokenId = 0; + let accumulatedData = ""; // Buffer to accumulate data chunks + + while (!stop) { + // Read the stream and log the outputs to console + const out = (await reader?.read()) ?? { done: false, value: undefined }; + + // If it's done, we cancel + if (out.done) { + reader?.cancel(); + return; + } + + if (!out.value) { + return; + } + + // Accumulate the data chunk + accumulatedData += out.value; + // Keep read data to check event type + const eventData = out.value; + + // Process each complete JSON object in the accumulated data + while (accumulatedData.includes("\n")) { + // Assuming each JSON object ends with a newline + const endIndex = accumulatedData.indexOf("\n"); + let jsonString = accumulatedData.substring(0, endIndex).trim(); + // Remove the processed part from the buffer + + accumulatedData = accumulatedData.substring(endIndex + 1); + + // Stopping with end event + if (eventData.startsWith("event: end")) { + stop = true; + yield { + token: { + id: tokenId++, + text: "", + logprob: 0, + special: true, + }, + generated_text: generatedText, + details: null, + } satisfies TextGenerationStreamOutput; + reader?.cancel(); + continue; + } + + if (eventData.startsWith("event: data") && jsonString.startsWith("data: ")) { + jsonString = jsonString.slice(6); + let data = null; + + // Handle the parsed data + try { + data = JSON.parse(jsonString); + } catch (e) { + logger.error(e, "Failed to parse JSON"); + logger.error(jsonString, "Problematic JSON string:"); + continue; // Skip this iteration and try the next chunk + } + // Assuming content within data is a plain string + if (data) { + generatedText += data; + const output: TextGenerationStreamOutput = { + token: { + id: tokenId++, + text: data, + logprob: 0, + special: false, + }, + generated_text: null, + details: null, + }; + yield output; + } + } + } + } + })(); + }; +} + +export default endpointLangserve; diff --git a/src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts b/src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts new file mode 100644 index 0000000000000000000000000000000000000000..944dbf4999652aeb8f4e5f8fd9374b9f9553f706 --- /dev/null +++ b/src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts @@ -0,0 +1,127 @@ +import { env } from "$env/dynamic/private"; +import { buildPrompt } from "$lib/buildPrompt"; +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import type { Endpoint } from "../endpoints"; +import { z } from "zod"; +import { logger } from "$lib/server/logger"; + +export const endpointLlamacppParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("llamacpp"), + url: z.string().url().default("http://127.0.0.1:8080"), // legacy, feel free to remove in breaking change update + baseURL: z.string().url().optional(), + accessToken: z.string().default(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN), +}); + +export function endpointLlamacpp( + input: z.input +): Endpoint { + const { baseURL, url, model } = endpointLlamacppParametersSchema.parse(input); + return async ({ messages, preprompt, continueMessage, generateSettings }) => { + const prompt = await buildPrompt({ + messages, + continueMessage, + preprompt, + model, + }); + + const parameters = { ...model.parameters, ...generateSettings }; + + const r = await fetch(`${baseURL ?? url}/completion`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + prompt, + stream: true, + temperature: parameters.temperature, + top_p: parameters.top_p, + top_k: parameters.top_k, + stop: parameters.stop, + repeat_penalty: parameters.repetition_penalty, + n_predict: parameters.max_new_tokens, + cache_prompt: true, + }), + }); + + if (!r.ok) { + throw new Error(`Failed to generate text: ${await r.text()}`); + } + + const encoder = new TextDecoderStream(); + const reader = r.body?.pipeThrough(encoder).getReader(); + + return (async function* () { + let stop = false; + let generatedText = ""; + let tokenId = 0; + let accumulatedData = ""; // Buffer to accumulate data chunks + + while (!stop) { + // Read the stream and log the outputs to console + const out = (await reader?.read()) ?? { done: false, value: undefined }; + + // If it's done, we cancel + if (out.done) { + reader?.cancel(); + return; + } + + if (!out.value) { + return; + } + + // Accumulate the data chunk + accumulatedData += out.value; + + // Process each complete JSON object in the accumulated data + while (accumulatedData.includes("\n")) { + // Assuming each JSON object ends with a newline + const endIndex = accumulatedData.indexOf("\n"); + let jsonString = accumulatedData.substring(0, endIndex).trim(); + + // Remove the processed part from the buffer + accumulatedData = accumulatedData.substring(endIndex + 1); + + if (jsonString.startsWith("data: ")) { + jsonString = jsonString.slice(6); + let data = null; + + try { + data = JSON.parse(jsonString); + } catch (e) { + logger.error(e, "Failed to parse JSON"); + logger.error(jsonString, "Problematic JSON string:"); + continue; // Skip this iteration and try the next chunk + } + + // Handle the parsed data + if (data.content || data.stop) { + generatedText += data.content; + const output: TextGenerationStreamOutput = { + token: { + id: tokenId++, + text: data.content ?? "", + logprob: 0, + special: false, + }, + generated_text: data.stop ? generatedText : null, + details: null, + }; + if (data.stop) { + stop = true; + output.token.special = true; + reader?.cancel(); + } + yield output; + } + } + } + } + })(); + }; +} + +export default endpointLlamacpp; diff --git a/src/lib/server/endpoints/ollama/endpointOllama.ts b/src/lib/server/endpoints/ollama/endpointOllama.ts new file mode 100644 index 0000000000000000000000000000000000000000..8c64f93a141f147a9e877e7bf094b067f12609b7 --- /dev/null +++ b/src/lib/server/endpoints/ollama/endpointOllama.ts @@ -0,0 +1,133 @@ +import { buildPrompt } from "$lib/buildPrompt"; +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import type { Endpoint } from "../endpoints"; +import { z } from "zod"; + +export const endpointOllamaParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("ollama"), + url: z.string().url().default("http://127.0.0.1:11434"), + ollamaName: z.string().min(1).optional(), +}); + +export function endpointOllama(input: z.input): Endpoint { + const { url, model, ollamaName } = endpointOllamaParametersSchema.parse(input); + + return async ({ messages, preprompt, continueMessage, generateSettings }) => { + const prompt = await buildPrompt({ + messages, + continueMessage, + preprompt, + model, + }); + + const parameters = { ...model.parameters, ...generateSettings }; + + const requestInfo = await fetch(`${url}/api/tags`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + const tags = await requestInfo.json(); + + if (!tags.models.some((m: { name: string }) => m.name === ollamaName)) { + // if its not in the tags, pull but dont wait for the answer + fetch(`${url}/api/pull`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: ollamaName ?? model.name, + stream: false, + }), + }); + + throw new Error("Currently pulling model from Ollama, please try again later."); + } + + const r = await fetch(`${url}/api/generate`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + prompt, + model: ollamaName ?? model.name, + raw: true, + options: { + top_p: parameters.top_p, + top_k: parameters.top_k, + temperature: parameters.temperature, + repeat_penalty: parameters.repetition_penalty, + stop: parameters.stop, + num_predict: parameters.max_new_tokens, + }, + }), + }); + + if (!r.ok) { + throw new Error(`Failed to generate text: ${await r.text()}`); + } + + const encoder = new TextDecoderStream(); + const reader = r.body?.pipeThrough(encoder).getReader(); + + return (async function* () { + let generatedText = ""; + let tokenId = 0; + let stop = false; + while (!stop) { + // read the stream and log the outputs to console + const out = (await reader?.read()) ?? { done: false, value: undefined }; + // we read, if it's done we cancel + if (out.done) { + reader?.cancel(); + return; + } + + if (!out.value) { + return; + } + + let data = null; + try { + data = JSON.parse(out.value); + } catch (e) { + return; + } + if (!data.done) { + generatedText += data.response; + + yield { + token: { + id: tokenId++, + text: data.response ?? "", + logprob: 0, + special: false, + }, + generated_text: null, + details: null, + } satisfies TextGenerationStreamOutput; + } else { + stop = true; + yield { + token: { + id: tokenId++, + text: data.response ?? "", + logprob: 0, + special: true, + }, + generated_text: generatedText, + details: null, + } satisfies TextGenerationStreamOutput; + } + } + })(); + }; +} + +export default endpointOllama; diff --git a/src/lib/server/endpoints/openai/endpointOai.ts b/src/lib/server/endpoints/openai/endpointOai.ts new file mode 100644 index 0000000000000000000000000000000000000000..be1b2769ee2b82c295fb3de6db4a76f12db4285c --- /dev/null +++ b/src/lib/server/endpoints/openai/endpointOai.ts @@ -0,0 +1,341 @@ +import { z } from "zod"; +import { openAICompletionToTextGenerationStream } from "./openAICompletionToTextGenerationStream"; +import { + openAIChatToTextGenerationSingle, + openAIChatToTextGenerationStream, +} from "./openAIChatToTextGenerationStream"; +import type { CompletionCreateParamsStreaming } from "openai/resources/completions"; +import type { + ChatCompletionCreateParamsNonStreaming, + ChatCompletionCreateParamsStreaming, + ChatCompletionTool, +} from "openai/resources/chat/completions"; +import type { FunctionDefinition, FunctionParameters } from "openai/resources/shared"; +import { buildPrompt } from "$lib/buildPrompt"; +import { env } from "$env/dynamic/private"; +import type { Endpoint } from "../endpoints"; +import type OpenAI from "openai"; +import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images"; +import type { MessageFile } from "$lib/types/Message"; +import { type Tool } from "$lib/types/Tool"; +import type { EndpointMessage } from "../endpoints"; +import { v4 as uuidv4 } from "uuid"; +function createChatCompletionToolsArray(tools: Tool[] | undefined): ChatCompletionTool[] { + const toolChoices = [] as ChatCompletionTool[]; + if (tools === undefined) { + return toolChoices; + } + + for (const t of tools) { + const requiredProperties = [] as string[]; + + const properties = {} as Record; + for (const idx in t.inputs) { + const parameterDefinition = t.inputs[idx]; + + const parameter = {} as Record; + switch (parameterDefinition.type) { + case "str": + parameter.type = "string"; + break; + case "float": + case "int": + parameter.type = "number"; + break; + case "bool": + parameter.type = "boolean"; + break; + case "file": + throw new Error("File type's currently not supported"); + default: + throw new Error(`Unknown tool IO type: ${t}`); + } + + if ("description" in parameterDefinition) { + parameter.description = parameterDefinition.description; + } + + if (parameterDefinition.paramType == "required") { + requiredProperties.push(t.inputs[idx].name); + } + + properties[t.inputs[idx].name] = parameter; + } + + const functionParameters: FunctionParameters = { + type: "object", + ...(requiredProperties.length > 0 ? { required: requiredProperties } : {}), + properties, + }; + + const functionDefinition: FunctionDefinition = { + name: t.name, + description: t.description, + parameters: functionParameters, + }; + + const toolDefinition: ChatCompletionTool = { + type: "function", + function: functionDefinition, + }; + + toolChoices.push(toolDefinition); + } + + return toolChoices; +} + +export const endpointOAIParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("openai"), + baseURL: z.string().url().default("https://api.openai.com/v1"), + apiKey: z.string().default(env.OPENAI_API_KEY || env.HF_TOKEN || "sk-"), + completion: z + .union([z.literal("completions"), z.literal("chat_completions")]) + .default("chat_completions"), + defaultHeaders: z.record(z.string()).optional(), + defaultQuery: z.record(z.string()).optional(), + extraBody: z.record(z.any()).optional(), + multimodal: z + .object({ + image: createImageProcessorOptionsValidator({ + supportedMimeTypes: [ + "image/png", + "image/jpeg", + "image/webp", + "image/avif", + "image/tiff", + "image/gif", + ], + preferredMimeType: "image/webp", + maxSizeInMB: Infinity, + maxWidth: 4096, + maxHeight: 4096, + }), + }) + .default({}), + /* enable use of max_completion_tokens in place of max_tokens */ + useCompletionTokens: z.boolean().default(false), + streamingSupported: z.boolean().default(true), +}); + +export async function endpointOai( + input: z.input +): Promise { + const { + baseURL, + apiKey, + completion, + model, + defaultHeaders, + defaultQuery, + multimodal, + extraBody, + useCompletionTokens, + streamingSupported, + } = endpointOAIParametersSchema.parse(input); + + let OpenAI; + try { + OpenAI = (await import("openai")).OpenAI; + } catch (e) { + throw new Error("Failed to import OpenAI", { cause: e }); + } + + const openai = new OpenAI({ + apiKey: apiKey || "sk-", + baseURL, + defaultHeaders, + defaultQuery, + }); + + const imageProcessor = makeImageProcessor(multimodal.image); + + if (completion === "completions") { + if (model.tools) { + throw new Error( + "Tools are not supported for 'completions' mode, switch to 'chat_completions' instead" + ); + } + return async ({ messages, preprompt, continueMessage, generateSettings, conversationId }) => { + const prompt = await buildPrompt({ + messages, + continueMessage, + preprompt, + model, + }); + + const parameters = { ...model.parameters, ...generateSettings }; + const body: CompletionCreateParamsStreaming = { + model: model.id ?? model.name, + prompt, + stream: true, + max_tokens: parameters?.max_new_tokens, + stop: parameters?.stop, + temperature: parameters?.temperature, + top_p: parameters?.top_p, + frequency_penalty: parameters?.repetition_penalty, + presence_penalty: parameters?.presence_penalty, + }; + + const openAICompletion = await openai.completions.create(body, { + body: { ...body, ...extraBody }, + headers: { + "ChatUI-Conversation-ID": conversationId?.toString() ?? "", + "X-use-cache": "false", + }, + }); + + return openAICompletionToTextGenerationStream(openAICompletion); + }; + } else if (completion === "chat_completions") { + return async ({ + messages, + preprompt, + generateSettings, + tools, + toolResults, + conversationId, + }) => { + let messagesOpenAI: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = + await prepareMessages(messages, imageProcessor, !model.tools && model.multimodal); + + if (messagesOpenAI?.[0]?.role !== "system") { + messagesOpenAI = [{ role: "system", content: "" }, ...messagesOpenAI]; + } + + if (messagesOpenAI?.[0]) { + messagesOpenAI[0].content = preprompt ?? ""; + } + + // if system role is not supported, convert first message to a user message. + if (!model.systemRoleSupported && messagesOpenAI?.[0]?.role === "system") { + messagesOpenAI[0] = { + ...messagesOpenAI[0], + role: "user", + }; + } + + if (toolResults && toolResults.length > 0) { + const toolCallRequests: OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam = { + role: "assistant", + content: null, + tool_calls: [], + }; + + const responses: Array = []; + + for (const result of toolResults) { + const id = uuidv4(); + + const toolCallResult: OpenAI.Chat.Completions.ChatCompletionMessageToolCall = { + type: "function", + function: { + name: result.call.name, + arguments: JSON.stringify(result.call.parameters), + }, + id, + }; + + toolCallRequests.tool_calls?.push(toolCallResult); + const toolCallResponse: OpenAI.Chat.Completions.ChatCompletionToolMessageParam = { + role: "tool", + content: "", + tool_call_id: id, + }; + if ("outputs" in result) { + toolCallResponse.content = JSON.stringify(result.outputs); + } + responses.push(toolCallResponse); + } + messagesOpenAI.push(toolCallRequests); + messagesOpenAI.push(...responses); + } + + const parameters = { ...model.parameters, ...generateSettings }; + const toolCallChoices = createChatCompletionToolsArray(tools); + const body = { + model: model.id ?? model.name, + messages: messagesOpenAI, + stream: streamingSupported, + ...(useCompletionTokens + ? { max_completion_tokens: parameters?.max_new_tokens } + : { max_tokens: parameters?.max_new_tokens }), + stop: parameters?.stop, + temperature: parameters?.temperature, + top_p: parameters?.top_p, + frequency_penalty: parameters?.repetition_penalty, + presence_penalty: parameters?.presence_penalty, + ...(toolCallChoices.length > 0 ? { tools: toolCallChoices, tool_choice: "auto" } : {}), + }; + + if (streamingSupported) { + const openChatAICompletion = await openai.chat.completions.create( + body as ChatCompletionCreateParamsStreaming, + { + body: { ...body, ...extraBody }, + headers: { + "ChatUI-Conversation-ID": conversationId?.toString() ?? "", + "X-use-cache": "false", + }, + } + ); + return openAIChatToTextGenerationStream(openChatAICompletion); + } else { + const openChatAICompletion = await openai.chat.completions.create( + body as ChatCompletionCreateParamsNonStreaming, + { + body: { ...body, ...extraBody }, + headers: { + "ChatUI-Conversation-ID": conversationId?.toString() ?? "", + "X-use-cache": "false", + }, + } + ); + return openAIChatToTextGenerationSingle(openChatAICompletion); + } + }; + } else { + throw new Error("Invalid completion type"); + } +} + +async function prepareMessages( + messages: EndpointMessage[], + imageProcessor: ReturnType, + isMultimodal: boolean +): Promise { + return Promise.all( + messages.map(async (message) => { + if (message.from === "user" && isMultimodal) { + return { + role: message.from, + content: [ + ...(await prepareFiles(imageProcessor, message.files ?? [])), + { type: "text", text: message.content }, + ], + }; + } + return { + role: message.from, + content: message.content, + }; + }) + ); +} + +async function prepareFiles( + imageProcessor: ReturnType, + files: MessageFile[] +): Promise { + const processedFiles = await Promise.all( + files.filter((file) => file.mime.startsWith("image/")).map(imageProcessor) + ); + return processedFiles.map((file) => ({ + type: "image_url" as const, + image_url: { + url: `data:${file.mime};base64,${file.image.toString("base64")}`, + }, + })); +} diff --git a/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts b/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a8ed629b4e334e99ca0e80fd1f4d76ee9b19a6e --- /dev/null +++ b/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts @@ -0,0 +1,155 @@ +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import type OpenAI from "openai"; +import type { Stream } from "openai/streaming"; +import type { ToolCall } from "$lib/types/Tool"; + +type ToolCallWithParameters = { + toolCall: ToolCall; + parameterJsonString: string; +}; + +function prepareToolCalls(toolCallsWithParameters: ToolCallWithParameters[], tokenId: number) { + const toolCalls: ToolCall[] = []; + + for (const toolCallWithParameters of toolCallsWithParameters) { + // HACK: sometimes gpt4 via azure returns the JSON with literal newlines in it + // like {\n "foo": "bar" } + const s = toolCallWithParameters.parameterJsonString.replace("\n", ""); + const params = JSON.parse(s); + + const toolCall = toolCallWithParameters.toolCall; + for (const name in params) { + toolCall.parameters[name] = params[name]; + } + + toolCalls.push(toolCall); + } + + const output = { + token: { + id: tokenId, + text: "", + logprob: 0, + special: false, + toolCalls, + }, + generated_text: null, + details: null, + }; + + return output; +} + +/** + * Transform a stream of OpenAI.Chat.ChatCompletion into a stream of TextGenerationStreamOutput + */ +export async function* openAIChatToTextGenerationStream( + completionStream: Stream +) { + let generatedText = ""; + let tokenId = 0; + const toolCalls: ToolCallWithParameters[] = []; + let toolBuffer = ""; // XXX: hack because tools seem broken on tgi openai endpoints? + + for await (const completion of completionStream) { + const { choices } = completion; + const content = choices[0]?.delta?.content ?? ""; + const last = choices[0]?.finish_reason === "stop" || choices[0]?.finish_reason === "length"; + + // if the last token is a stop and the tool buffer is not empty, yield it as a generated_text + if (choices[0]?.finish_reason === "stop" && toolBuffer.length > 0) { + yield { + token: { + id: tokenId++, + special: true, + logprob: 0, + text: "", + }, + generated_text: toolBuffer, + details: null, + } as TextGenerationStreamOutput; + break; + } + + // weird bug where the parameters are streamed in like this + if (choices[0]?.delta?.tool_calls) { + const calls = Array.isArray(choices[0].delta.tool_calls) + ? choices[0].delta.tool_calls + : [choices[0].delta.tool_calls]; + + if ( + calls.length === 1 && + calls[0].index === 0 && + calls[0].id === "" && + calls[0].type === "function" && + !!calls[0].function && + calls[0].function.name === null + ) { + toolBuffer += calls[0].function.arguments; + continue; + } + } + + if (content) { + generatedText = generatedText + content; + } + const output: TextGenerationStreamOutput = { + token: { + id: tokenId++, + text: content ?? "", + logprob: 0, + special: last, + }, + generated_text: last ? generatedText : null, + details: null, + }; + yield output; + + const tools = completion.choices[0]?.delta?.tool_calls || []; + for (const tool of tools) { + if (tool.id) { + if (!tool.function?.name) { + throw new Error("Tool call without function name"); + } + const toolCallWithParameters: ToolCallWithParameters = { + toolCall: { + name: tool.function.name, + parameters: {}, + }, + parameterJsonString: "", + }; + toolCalls.push(toolCallWithParameters); + } + + if (toolCalls.length > 0 && tool.function?.arguments) { + toolCalls[toolCalls.length - 1].parameterJsonString += tool.function.arguments; + } + } + + if (choices[0]?.finish_reason === "tool_calls") { + yield prepareToolCalls(toolCalls, tokenId++); + } + } +} + +/** + * Transform a non-streaming OpenAI chat completion into a stream of TextGenerationStreamOutput + */ +export async function* openAIChatToTextGenerationSingle( + completion: OpenAI.Chat.Completions.ChatCompletion +) { + const content = completion.choices[0]?.message?.content || ""; + const tokenId = 0; + + // Yield the content as a single token + yield { + token: { + id: tokenId, + text: content, + logprob: 0, + special: false, + }, + generated_text: content, + details: null, + } as TextGenerationStreamOutput; +} diff --git a/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts b/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts new file mode 100644 index 0000000000000000000000000000000000000000..517eada9a772a54d0405260acd6ff590700c6048 --- /dev/null +++ b/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts @@ -0,0 +1,32 @@ +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import type OpenAI from "openai"; +import type { Stream } from "openai/streaming"; + +/** + * Transform a stream of OpenAI.Completions.Completion into a stream of TextGenerationStreamOutput + */ +export async function* openAICompletionToTextGenerationStream( + completionStream: Stream +) { + let generatedText = ""; + let tokenId = 0; + for await (const completion of completionStream) { + const { choices } = completion; + const text = choices[0]?.text ?? ""; + const last = choices[0]?.finish_reason === "stop" || choices[0]?.finish_reason === "length"; + if (text) { + generatedText = generatedText + text; + } + const output: TextGenerationStreamOutput = { + token: { + id: tokenId++, + text, + logprob: 0, + special: last, + }, + generated_text: last ? generatedText : null, + details: null, + }; + yield output; + } +} diff --git a/src/lib/server/endpoints/preprocessMessages.ts b/src/lib/server/endpoints/preprocessMessages.ts new file mode 100644 index 0000000000000000000000000000000000000000..d4e272c460ea6297d3cf14ae4833c03b0417e808 --- /dev/null +++ b/src/lib/server/endpoints/preprocessMessages.ts @@ -0,0 +1,76 @@ +import type { Message } from "$lib/types/Message"; +import { format } from "date-fns"; +import type { EndpointMessage } from "./endpoints"; +import { downloadFile } from "../files/downloadFile"; +import type { ObjectId } from "mongodb"; + +export async function preprocessMessages( + messages: Message[], + webSearch: Message["webSearch"], + convId: ObjectId +): Promise { + return Promise.resolve(messages) + .then((msgs) => addWebSearchContext(msgs, webSearch)) + .then((msgs) => downloadFiles(msgs, convId)) + .then((msgs) => injectClipboardFiles(msgs)); +} + +function addWebSearchContext(messages: Message[], webSearch: Message["webSearch"]) { + const webSearchContext = webSearch?.contextSources + .map(({ context }, idx) => `Source [${idx + 1}]\n${context.trim()}`) + .join("\n\n----------\n\n"); + + // No web search context available, skip + if (!webSearch || !webSearchContext?.trim()) return messages; + // No messages available, skip + if (messages.length === 0) return messages; + + const lastQuestion = messages.findLast((el) => el.from === "user")?.content ?? ""; + const previousQuestions = messages + .filter((el) => el.from === "user") + .slice(0, -1) + .map((el) => el.content); + const currentDate = format(new Date(), "MMMM d, yyyy"); + + const finalMessage = { + ...messages[messages.length - 1], + content: `I searched the web using the query: ${webSearch.searchQuery}. The query was generated by a tool and might not be relevant to the question. +Today is ${currentDate} and here are the results. +When answering the question, you must reference the sources you used inline by wrapping the index in brackets like this: [1]. If multiple sources are used, you must reference each one of them without commas like this: [1][2][3]. +===================== +${webSearchContext} +===================== +${previousQuestions.length > 0 ? `Previous questions: \n- ${previousQuestions.join("\n- ")}` : ""} +Answer the question: ${lastQuestion}`, + }; + + return [...messages.slice(0, -1), finalMessage]; +} + +async function downloadFiles(messages: Message[], convId: ObjectId): Promise { + return Promise.all( + messages.map>((message) => + Promise.all((message.files ?? []).map((file) => downloadFile(file.value, convId))).then( + (files) => ({ ...message, files }) + ) + ) + ); +} + +async function injectClipboardFiles(messages: EndpointMessage[]) { + return Promise.all( + messages.map((message) => { + const plaintextFiles = message.files + ?.filter((file) => file.mime === "application/vnd.chatui.clipboard") + .map((file) => Buffer.from(file.value, "base64").toString("utf-8")); + + if (!plaintextFiles || plaintextFiles.length === 0) return message; + + return { + ...message, + content: `${plaintextFiles.join("\n\n")}\n\n${message.content}`, + files: message.files?.filter((file) => file.mime !== "application/vnd.chatui.clipboard"), + }; + }) + ); +} diff --git a/src/lib/server/endpoints/tgi/endpointTgi.ts b/src/lib/server/endpoints/tgi/endpointTgi.ts new file mode 100644 index 0000000000000000000000000000000000000000..a9e6d47a6de52cfbb4fc471003002c9af7af1d32 --- /dev/null +++ b/src/lib/server/endpoints/tgi/endpointTgi.ts @@ -0,0 +1,99 @@ +import { env } from "$env/dynamic/private"; +import { buildPrompt } from "$lib/buildPrompt"; +import { textGenerationStream } from "@huggingface/inference"; +import type { Endpoint, EndpointMessage } from "../endpoints"; +import { z } from "zod"; +import { + createImageProcessorOptionsValidator, + makeImageProcessor, + type ImageProcessor, +} from "../images"; + +export const endpointTgiParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("tgi"), + url: z.string().url(), + accessToken: z.string().default(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN), + authorization: z.string().optional(), + multimodal: z + .object({ + // Assumes IDEFICS + image: createImageProcessorOptionsValidator({ + supportedMimeTypes: ["image/jpeg", "image/webp"], + preferredMimeType: "image/webp", + maxSizeInMB: 5, + maxWidth: 378, + maxHeight: 980, + }), + }) + .default({}), +}); + +export function endpointTgi(input: z.input): Endpoint { + const { url, accessToken, model, authorization, multimodal } = + endpointTgiParametersSchema.parse(input); + const imageProcessor = makeImageProcessor(multimodal.image); + + return async ({ + messages, + preprompt, + continueMessage, + generateSettings, + tools, + toolResults, + isMultimodal, + conversationId, + }) => { + const messagesWithResizedFiles = await Promise.all( + messages.map((message) => prepareMessage(Boolean(isMultimodal), message, imageProcessor)) + ); + + const prompt = await buildPrompt({ + messages: messagesWithResizedFiles, + preprompt, + model, + continueMessage, + tools, + toolResults, + }); + + return textGenerationStream( + { + parameters: { ...model.parameters, ...generateSettings, return_full_text: false }, + model: url, + inputs: prompt, + accessToken, + }, + { + use_cache: false, + fetch: async (endpointUrl, info) => { + if (info && authorization && !accessToken) { + // Set authorization header if it is defined and HF_TOKEN is empty + info.headers = { + ...info.headers, + Authorization: authorization, + "ChatUI-Conversation-ID": conversationId?.toString() ?? "", + }; + } + return fetch(endpointUrl, info); + }, + } + ); + }; +} + +async function prepareMessage( + isMultimodal: boolean, + message: EndpointMessage, + imageProcessor: ImageProcessor +): Promise { + if (!isMultimodal) return message; + const files = await Promise.all(message.files?.map(imageProcessor) ?? []); + const markdowns = files.map( + (file) => `![](data:${file.mime};base64,${file.image.toString("base64")})` + ); + const content = message.content + "\n" + markdowns.join("\n "); + + return { ...message, content }; +} diff --git a/src/lib/server/exitHandler.ts b/src/lib/server/exitHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..892db73961989e881eb291f40ed31c9dffeeaef2 --- /dev/null +++ b/src/lib/server/exitHandler.ts @@ -0,0 +1,41 @@ +import { randomUUID } from "$lib/utils/randomUuid"; +import { timeout } from "$lib/utils/timeout"; +import { logger } from "./logger"; + +type ExitHandler = () => void | Promise; +type ExitHandlerUnsubscribe = () => void; + +const listeners = new Map(); + +export function onExit(cb: ExitHandler): ExitHandlerUnsubscribe { + const uuid = randomUUID(); + listeners.set(uuid, cb); + return () => { + listeners.delete(uuid); + }; +} + +async function runExitHandler(handler: ExitHandler): Promise { + return timeout(Promise.resolve().then(handler), 30_000).catch((err) => { + logger.error(err, "Exit handler failed to run"); + }); +} + +export function initExitHandler() { + let signalCount = 0; + const exitHandler = async () => { + signalCount++; + if (signalCount === 1) { + logger.info("Received signal... Exiting"); + await Promise.all(Array.from(listeners.values()).map(runExitHandler)); + logger.info("All exit handlers ran... Waiting for svelte server to exit"); + } + if (signalCount === 3) { + logger.warn("Received 3 signals... Exiting immediately"); + process.exit(1); + } + }; + + process.on("SIGINT", exitHandler); + process.on("SIGTERM", exitHandler); +} diff --git a/src/lib/server/files/downloadFile.ts b/src/lib/server/files/downloadFile.ts new file mode 100644 index 0000000000000000000000000000000000000000..d289fc10c85220834b2828ebe4cf32b0776ccc73 --- /dev/null +++ b/src/lib/server/files/downloadFile.ts @@ -0,0 +1,34 @@ +import { error } from "@sveltejs/kit"; +import { collections } from "$lib/server/database"; +import type { Conversation } from "$lib/types/Conversation"; +import type { SharedConversation } from "$lib/types/SharedConversation"; +import type { MessageFile } from "$lib/types/Message"; + +export async function downloadFile( + sha256: string, + convId: Conversation["_id"] | SharedConversation["_id"] +): Promise { + const fileId = collections.bucket.find({ filename: `${convId.toString()}-${sha256}` }); + + const file = await fileId.next(); + if (!file) { + error(404, "File not found"); + } + if (file.metadata?.conversation !== convId.toString()) { + error(403, "You don't have access to this file."); + } + + const mime = file.metadata?.mime; + const name = file.filename; + + const fileStream = collections.bucket.openDownloadStream(file._id); + + const buffer = await new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + fileStream.on("data", (chunk) => chunks.push(chunk)); + fileStream.on("error", reject); + fileStream.on("end", () => resolve(Buffer.concat(chunks))); + }); + + return { type: "base64", name, value: buffer.toString("base64"), mime }; +} diff --git a/src/lib/server/files/uploadFile.ts b/src/lib/server/files/uploadFile.ts new file mode 100644 index 0000000000000000000000000000000000000000..97b335beaf00cbad96ce1ef3bbd531cd8ce129ba --- /dev/null +++ b/src/lib/server/files/uploadFile.ts @@ -0,0 +1,29 @@ +import type { Conversation } from "$lib/types/Conversation"; +import type { MessageFile } from "$lib/types/Message"; +import { sha256 } from "$lib/utils/sha256"; +import { fileTypeFromBuffer } from "file-type"; +import { collections } from "$lib/server/database"; + +export async function uploadFile(file: File, conv: Conversation): Promise { + const sha = await sha256(await file.text()); + const buffer = await file.arrayBuffer(); + + // Attempt to detect the mime type of the file, fallback to the uploaded mime + const mime = await fileTypeFromBuffer(buffer).then((fileType) => fileType?.mime ?? file.type); + + const upload = collections.bucket.openUploadStream(`${conv._id}-${sha}`, { + metadata: { conversation: conv._id.toString(), mime }, + }); + + upload.write((await file.arrayBuffer()) as unknown as Buffer); + upload.end(); + + // only return the filename when upload throws a finish event or a 20s time out occurs + return new Promise((resolve, reject) => { + upload.once("finish", () => + resolve({ type: "hash", value: sha, mime: file.type, name: file.name }) + ); + upload.once("error", reject); + setTimeout(() => reject(new Error("Upload timed out")), 20_000); + }); +} diff --git a/src/lib/server/fonts/Inter-Black.ttf b/src/lib/server/fonts/Inter-Black.ttf new file mode 100644 index 0000000000000000000000000000000000000000..04c941d41452e12fdaff70474a07b6cf5d535e81 --- /dev/null +++ b/src/lib/server/fonts/Inter-Black.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4795b76b5b54d140fa17432eb4ee2eb27c63156ca0c8184ed27c4781faafe276 +size 316848 diff --git a/src/lib/server/fonts/Inter-Bold.ttf b/src/lib/server/fonts/Inter-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..bdaa1b04059fbcd486ba30745f2ba9dd7fdd7fe7 --- /dev/null +++ b/src/lib/server/fonts/Inter-Bold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:412c068eab6f36e6807d630ff89127165e8e4d3e8653434cdfb56b60cdcc3a32 +size 316584 diff --git a/src/lib/server/fonts/Inter-ExtraBold.ttf b/src/lib/server/fonts/Inter-ExtraBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7e35c7eb917acb90b78246d164ba0f892241e103 --- /dev/null +++ b/src/lib/server/fonts/Inter-ExtraBold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d78d9777567fc7320968861417653cbbb80d861f0dfd9978e9705b4400696910 +size 317184 diff --git a/src/lib/server/fonts/Inter-ExtraLight.ttf b/src/lib/server/fonts/Inter-ExtraLight.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a0314633dc4cd20edabbc131787f8d1d3711b6de --- /dev/null +++ b/src/lib/server/fonts/Inter-ExtraLight.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3be0e36c828b773e3f10568461f3a0baf7323cff772d9408df04222a205bcb1f +size 311232 diff --git a/src/lib/server/fonts/Inter-Light.ttf b/src/lib/server/fonts/Inter-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..296538a0afbdf3214413c0f449a3b7c63667220c --- /dev/null +++ b/src/lib/server/fonts/Inter-Light.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a04215a19659c1cfdf462157fc69efa03df8cc67c7353f83d80f8ead7698a169 +size 310832 diff --git a/src/lib/server/fonts/Inter-Medium.ttf b/src/lib/server/fonts/Inter-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..53e8b53bd03c373d8e46bd9f7ca8d7d9ce3190ab --- /dev/null +++ b/src/lib/server/fonts/Inter-Medium.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a645f55492d1c8cdace43c72be8cbec08e680b5a86d8b4c2d1c50d6e41e9cc96 +size 315132 diff --git a/src/lib/server/fonts/Inter-Regular.ttf b/src/lib/server/fonts/Inter-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b9750f31378a6adddd64fc5273cd2399ef6b415d --- /dev/null +++ b/src/lib/server/fonts/Inter-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3127f0b873387ee37e2040135a06e9e9c05030f509eb63689529becf28b50384 +size 310252 diff --git a/src/lib/server/fonts/Inter-SemiBold.ttf b/src/lib/server/fonts/Inter-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2912df575fe6e1fe4ee807ab2fe2cccf4458d3d5 --- /dev/null +++ b/src/lib/server/fonts/Inter-SemiBold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0b540e69bf6717016e33874670e09acf4bffc2ca3f4c1cf174a4ff696308c65 +size 316220 diff --git a/src/lib/server/fonts/Inter-Thin.ttf b/src/lib/server/fonts/Inter-Thin.ttf new file mode 100644 index 0000000000000000000000000000000000000000..53304e56ad7e5ae667b3e8c680530e944ed06d39 --- /dev/null +++ b/src/lib/server/fonts/Inter-Thin.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9406f2adbb821d34651f66265b24bf67ed1731ac4133da8eb56270956009434f +size 310984 diff --git a/src/lib/server/generateFromDefaultEndpoint.ts b/src/lib/server/generateFromDefaultEndpoint.ts new file mode 100644 index 0000000000000000000000000000000000000000..48f0110b9e1b0dbc003ceec6fa43683192fe7091 --- /dev/null +++ b/src/lib/server/generateFromDefaultEndpoint.ts @@ -0,0 +1,35 @@ +import { smallModel } from "$lib/server/models"; +import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate"; +import type { EndpointMessage } from "./endpoints/endpoints"; + +export async function* generateFromDefaultEndpoint({ + messages, + preprompt, + generateSettings, +}: { + messages: EndpointMessage[]; + preprompt?: string; + generateSettings?: Record; +}): AsyncGenerator { + const endpoint = await smallModel.getEndpoint(); + + const tokenStream = await endpoint({ messages, preprompt, generateSettings }); + + for await (const output of tokenStream) { + // if not generated_text is here it means the generation is not done + if (output.generated_text) { + let generated_text = output.generated_text; + for (const stop of [...(smallModel.parameters?.stop ?? []), "<|endoftext|>"]) { + if (generated_text.endsWith(stop)) { + generated_text = generated_text.slice(0, -stop.length).trimEnd(); + } + } + return generated_text; + } + yield { + type: MessageUpdateType.Stream, + token: output.token.text, + }; + } + throw new Error("Generation failed"); +} diff --git a/src/lib/server/isURLLocal.spec.ts b/src/lib/server/isURLLocal.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2dda5f4b5aac82244095772a91980d936b8ef431 --- /dev/null +++ b/src/lib/server/isURLLocal.spec.ts @@ -0,0 +1,31 @@ +import { isURLLocal } from "./isURLLocal"; +import { describe, expect, it } from "vitest"; + +describe("isURLLocal", async () => { + it("should return true for localhost", async () => { + expect(await isURLLocal(new URL("http://localhost"))).toBe(true); + }); + it("should return true for 127.0.0.1", async () => { + expect(await isURLLocal(new URL("http://127.0.0.1"))).toBe(true); + }); + it("should return true for 127.254.254.254", async () => { + expect(await isURLLocal(new URL("http://127.254.254.254"))).toBe(true); + }); + it("should return false for huggingface.co", async () => { + expect(await isURLLocal(new URL("https://huggingface.co/"))).toBe(false); + }); + it("should return true for 127.0.0.1.nip.io", async () => { + expect(await isURLLocal(new URL("http://127.0.0.1.nip.io"))).toBe(true); + }); + it("should fail on ipv6", async () => { + await expect(isURLLocal(new URL("http://[::1]"))).rejects.toThrow(); + }); + it("should fail on ipv6 --1.sslip.io", async () => { + await expect(isURLLocal(new URL("http://--1.sslip.io"))).rejects.toThrow(); + }); + it("should fail on invalid domain names", async () => { + await expect( + isURLLocal(new URL("http://34329487239847329874923948732984.com/")) + ).rejects.toThrow(); + }); +}); diff --git a/src/lib/server/isURLLocal.ts b/src/lib/server/isURLLocal.ts new file mode 100644 index 0000000000000000000000000000000000000000..2512eca6dbd96590259dccc927352564a7a8cbaa --- /dev/null +++ b/src/lib/server/isURLLocal.ts @@ -0,0 +1,38 @@ +import { Address6, Address4 } from "ip-address"; +import dns from "node:dns"; + +const dnsLookup = (hostname: string): Promise<{ address: string; family: number }> => { + return new Promise((resolve, reject) => { + dns.lookup(hostname, (err, address, family) => { + if (err) return reject(err); + resolve({ address, family }); + }); + }); +}; + +export async function isURLLocal(URL: URL): Promise { + const { address, family } = await dnsLookup(URL.hostname); + + if (family === 4) { + const addr = new Address4(address); + const localSubnet = new Address4("127.0.0.0/8"); + return addr.isInSubnet(localSubnet); + } + + if (family === 6) { + const addr = new Address6(address); + return addr.isLoopback() || addr.isInSubnet(new Address6("::1/128")) || addr.isLinkLocal(); + } + + throw Error("Unknown IP family"); +} + +export function isURLStringLocal(url: string) { + try { + const urlObj = new URL(url); + return isURLLocal(urlObj); + } catch (e) { + // assume local if URL parsing fails + return true; + } +} diff --git a/src/lib/server/logger.ts b/src/lib/server/logger.ts new file mode 100644 index 0000000000000000000000000000000000000000..b01b7692e3a33b20df5a86572cd8d748113ffda7 --- /dev/null +++ b/src/lib/server/logger.ts @@ -0,0 +1,18 @@ +import pino from "pino"; +import { dev } from "$app/environment"; +import { env } from "$env/dynamic/private"; + +let options: pino.LoggerOptions = {}; + +if (dev) { + options = { + transport: { + target: "pino-pretty", + options: { + colorize: true, + }, + }, + }; +} + +export const logger = pino({ ...options, level: env.LOG_LEVEL ?? "info" }); diff --git a/src/lib/server/metrics.ts b/src/lib/server/metrics.ts new file mode 100644 index 0000000000000000000000000000000000000000..42d9fb9c84082fef2f621e554f202888a4ba7e33 --- /dev/null +++ b/src/lib/server/metrics.ts @@ -0,0 +1,206 @@ +import { collectDefaultMetrics, Registry, Counter, Summary } from "prom-client"; +import express from "express"; +import { logger } from "$lib/server/logger"; +import { env } from "$env/dynamic/private"; +import type { Model } from "$lib/types/Model"; +import { onExit } from "./exitHandler"; +import { promisify } from "util"; + +interface Metrics { + model: { + conversationsTotal: Counter; + messagesTotal: Counter; + tokenCountTotal: Counter; + timePerOutputToken: Summary; + timeToFirstToken: Summary; + latency: Summary; + votesPositive: Counter; + votesNegative: Counter; + }; + + webSearch: { + requestCount: Counter; + pageFetchCount: Counter; + pageFetchCountError: Counter; + pageFetchDuration: Summary; + embeddingDuration: Summary; + }; + + tool: { + toolUseCount: Counter; + toolUseCountError: Counter; + toolUseDuration: Summary; + timeToChooseTools: Summary; + }; +} + +export class MetricsServer { + private static instance: MetricsServer; + private metrics: Metrics; + + private constructor() { + const app = express(); + + const port = Number(env.METRICS_PORT || "5565"); + if (isNaN(port) || port < 0 || port > 65535) { + logger.warn(`Invalid value for METRICS_PORT: ${env.METRICS_PORT}`); + } + + if (env.METRICS_ENABLED !== "false" && env.METRICS_ENABLED !== "true") { + logger.warn(`Invalid value for METRICS_ENABLED: ${env.METRICS_ENABLED}`); + } + if (env.METRICS_ENABLED === "true") { + const server = app.listen(port, () => { + logger.info(`Metrics server listening on port ${port}`); + }); + const closeServer = promisify(server.close); + onExit(async () => { + logger.info("Disconnecting metrics server ..."); + await closeServer(); + logger.info("Server stopped ..."); + }); + } + + const register = new Registry(); + collectDefaultMetrics({ register }); + + this.metrics = { + model: { + conversationsTotal: new Counter({ + name: "model_conversations_total", + help: "Total number of conversations", + labelNames: ["model"], + registers: [register], + }), + messagesTotal: new Counter({ + name: "model_messages_total", + help: "Total number of messages", + labelNames: ["model"], + registers: [register], + }), + tokenCountTotal: new Counter({ + name: "model_token_count_total", + help: "Total number of tokens", + labelNames: ["model"], + registers: [register], + }), + timePerOutputToken: new Summary({ + name: "model_time_per_output_token_ms", + help: "Time per output token in ms", + labelNames: ["model"], + registers: [register], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + timeToFirstToken: new Summary({ + name: "model_time_to_first_token_ms", + help: "Time to first token", + labelNames: ["model"], + registers: [register], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + latency: new Summary({ + name: "model_latency_ms", + help: "Total latency until end of answer", + labelNames: ["model"], + registers: [register], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + votesPositive: new Counter({ + name: "model_votes_positive", + help: "Total number of positive votes on messages generated by the model", + labelNames: ["model"], + registers: [register], + }), + votesNegative: new Counter({ + name: "model_votes_negative", + help: "Total number of negative votes on messages generated by the model", + labelNames: ["model"], + registers: [register], + }), + }, + webSearch: { + requestCount: new Counter({ + name: "web_search_request_count", + help: "Total number of web search requests", + registers: [register], + }), + pageFetchCount: new Counter({ + name: "web_search_page_fetch_count", + help: "Total number of web search page fetches", + registers: [register], + }), + pageFetchCountError: new Counter({ + name: "web_search_page_fetch_count_error", + help: "Total number of web search page fetch errors", + registers: [register], + }), + pageFetchDuration: new Summary({ + name: "web_search_page_fetch_duration_ms", + help: "Web search page fetch duration", + registers: [register], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + embeddingDuration: new Summary({ + name: "web_search_embedding_duration_ms", + help: "Web search embedding duration", + registers: [register], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + }, + tool: { + toolUseCount: new Counter({ + name: "tool_use_count", + help: "Total number of tool uses", + labelNames: ["tool"], + registers: [register], + }), + toolUseCountError: new Counter({ + name: "tool_use_count_error", + help: "Total number of tool use errors", + labelNames: ["tool"], + registers: [register], + }), + toolUseDuration: new Summary({ + name: "tool_use_duration_ms", + help: "Tool use duration", + labelNames: ["tool"], + registers: [register], + maxAgeSeconds: 30 * 60, // longer duration since we use this to give feedback to the user + ageBuckets: 5, + }), + timeToChooseTools: new Summary({ + name: "time_to_choose_tools_ms", + help: "Time to choose tools", + labelNames: ["model"], + registers: [register], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + }, + }; + + app.get("/metrics", (req, res) => { + register.metrics().then((metrics) => { + res.set("Content-Type", "text/plain"); + res.send(metrics); + }); + }); + } + + public static getInstance(): MetricsServer { + if (!MetricsServer.instance) { + MetricsServer.instance = new MetricsServer(); + } + + return MetricsServer.instance; + } + + public static getMetrics(): Metrics { + return MetricsServer.getInstance().metrics; + } +} diff --git a/src/lib/server/models.ts b/src/lib/server/models.ts new file mode 100644 index 0000000000000000000000000000000000000000..be657d43ed2bf1d6f26eb4837bfd86237cd65702 --- /dev/null +++ b/src/lib/server/models.ts @@ -0,0 +1,407 @@ +import { env } from "$env/dynamic/private"; +import type { ChatTemplateInput } from "$lib/types/Template"; +import { compileTemplate } from "$lib/utils/template"; +import { z } from "zod"; +import endpoints, { endpointSchema, type Endpoint } from "./endpoints/endpoints"; +import { endpointTgi } from "./endpoints/tgi/endpointTgi"; +import { sum } from "$lib/utils/sum"; +import { embeddingModels, validateEmbeddingModelByName } from "./embeddingModels"; + +import type { PreTrainedTokenizer } from "@huggingface/transformers"; + +import JSON5 from "json5"; +import { getTokenizer } from "$lib/utils/getTokenizer"; +import { logger } from "$lib/server/logger"; +import { ToolResultStatus, type ToolInput } from "$lib/types/Tool"; +import { isHuggingChat } from "$lib/utils/isHuggingChat"; + +type Optional = Pick, K> & Omit; + +const reasoningSchema = z.union([ + z.object({ + type: z.literal("regex"), // everything is reasoning, extract the answer from the regex + regex: z.string(), + }), + z.object({ + type: z.literal("tokens"), // use beginning and end tokens that define the reasoning portion of the answer + beginToken: z.string(), // empty string means the model starts in reasoning mode + endToken: z.string(), + }), + z.object({ + type: z.literal("summarize"), // everything is reasoning, summarize the answer + }), +]); + +const modelConfig = z.object({ + /** Used as an identifier in DB */ + id: z.string().optional(), + /** Used to link to the model page, and for inference */ + name: z.string().default(""), + displayName: z.string().min(1).optional(), + description: z.string().min(1).optional(), + logoUrl: z.string().url().optional(), + websiteUrl: z.string().url().optional(), + modelUrl: z.string().url().optional(), + tokenizer: z + .union([ + z.string(), + z.object({ + tokenizerUrl: z.string().url(), + tokenizerConfigUrl: z.string().url(), + }), + ]) + .optional(), + datasetName: z.string().min(1).optional(), + datasetUrl: z.string().url().optional(), + preprompt: z.string().default(""), + prepromptUrl: z.string().url().optional(), + chatPromptTemplate: z.string().optional(), + promptExamples: z + .array( + z.object({ + title: z.string().min(1), + prompt: z.string().min(1), + }) + ) + .optional(), + endpoints: z.array(endpointSchema).optional(), + parameters: z + .object({ + temperature: z.number().min(0).max(2).optional(), + truncate: z.number().int().positive().optional(), + max_new_tokens: z.number().int().positive().optional(), + stop: z.array(z.string()).optional(), + top_p: z.number().positive().optional(), + top_k: z.number().positive().optional(), + repetition_penalty: z.number().min(-2).max(2).optional(), + presence_penalty: z.number().min(-2).max(2).optional(), + }) + .passthrough() + .optional(), + multimodal: z.boolean().default(false), + multimodalAcceptedMimetypes: z.array(z.string()).optional(), + tools: z.boolean().default(false), + unlisted: z.boolean().default(false), + embeddingModel: validateEmbeddingModelByName(embeddingModels).optional(), + /** Used to enable/disable system prompt usage */ + systemRoleSupported: z.boolean().default(true), + reasoning: reasoningSchema.optional(), +}); + +const modelsRaw = z.array(modelConfig).parse(JSON5.parse(env.MODELS)); + +async function getChatPromptRender( + m: z.infer +): Promise>> { + if (m.chatPromptTemplate) { + return compileTemplate(m.chatPromptTemplate, m); + } + let tokenizer: PreTrainedTokenizer; + + try { + tokenizer = await getTokenizer(m.tokenizer ?? m.id ?? m.name); + } catch (e) { + // if fetching the tokenizer fails but it wasnt manually set, use the default template + if (!m.tokenizer) { + logger.warn( + `No tokenizer found for model ${m.name}, using default template. Consider setting tokenizer manually or making sure the model is available on the hub.`, + m + ); + return compileTemplate( + "{{#if @root.preprompt}}<|im_start|>system\n{{@root.preprompt}}<|im_end|>\n{{/if}}{{#each messages}}{{#ifUser}}<|im_start|>user\n{{content}}<|im_end|>\n<|im_start|>assistant\n{{/ifUser}}{{#ifAssistant}}{{content}}<|im_end|>\n{{/ifAssistant}}{{/each}}", + m + ); + } + + logger.error( + e, + `Failed to load tokenizer ${ + m.tokenizer ?? m.id ?? m.name + } make sure the model is available on the hub and you have access to any gated models.` + ); + process.exit(); + } + + const renderTemplate = ({ + messages, + preprompt, + tools, + toolResults, + continueMessage, + }: ChatTemplateInput) => { + let formattedMessages: { + role: string; + content: string; + tool_calls?: { id: string; tool_call_id: string; output: string }[]; + }[] = messages.map((message) => ({ + content: message.content, + role: message.from, + })); + + if (!m.systemRoleSupported) { + const firstSystemMessage = formattedMessages.find((msg) => msg.role === "system"); + formattedMessages = formattedMessages.filter((msg) => msg.role !== "system"); + + if ( + firstSystemMessage && + formattedMessages.length > 0 && + formattedMessages[0].role === "user" + ) { + formattedMessages[0].content = + firstSystemMessage.content + "\n" + formattedMessages[0].content; + } + } + + if (preprompt && formattedMessages[0].role !== "system") { + formattedMessages = [ + { + role: m.systemRoleSupported ? "system" : "user", + content: preprompt, + }, + ...formattedMessages, + ]; + } + + if (toolResults?.length) { + // todo: should update the command r+ tokenizer to support system messages at any location + // or use the `rag` mode without the citations + const id = m.id ?? m.name; + + if (isHuggingChat && id.startsWith("CohereForAI")) { + formattedMessages = [ + { + role: "user", + content: + "\n\n\n" + + toolResults + .flatMap((result, idx) => { + if (result.status === ToolResultStatus.Error) { + return ( + `Document: ${idx}\n` + `Tool "${result.call.name}" error\n` + result.message + ); + } + return ( + `Document: ${idx}\n` + + result.outputs + .flatMap((output) => + Object.entries(output).map(([title, text]) => `${title}\n${text}`) + ) + .join("\n") + ); + }) + .join("\n\n") + + "\n", + }, + ...formattedMessages, + ]; + } else if (isHuggingChat && id.startsWith("meta-llama")) { + const results = toolResults.flatMap((result) => { + if (result.status === ToolResultStatus.Error) { + return [ + { + tool_call_id: result.call.name, + output: "Error: " + result.message, + }, + ]; + } else { + return result.outputs.map((output) => ({ + tool_call_id: result.call.name, + output: JSON.stringify(output), + })); + } + }); + + formattedMessages = [ + ...formattedMessages, + { + role: "python", + content: JSON.stringify(results), + }, + ]; + } else { + formattedMessages = [ + ...formattedMessages, + { + role: m.systemRoleSupported ? "system" : "user", + content: JSON.stringify(toolResults), + }, + ]; + } + tools = []; + } + + const mappedTools = + tools?.map((tool) => { + const inputs: Record< + string, + { + type: ToolInput["type"]; + description: string; + required: boolean; + } + > = {}; + + for (const value of tool.inputs) { + if (value.paramType !== "fixed") { + inputs[value.name] = { + type: value.type, + description: value.description ?? "", + required: value.paramType === "required", + }; + } + } + + return { + name: tool.name, + description: tool.description, + parameter_definitions: inputs, + }; + }) ?? []; + + const output = tokenizer.apply_chat_template(formattedMessages, { + tokenize: false, + add_generation_prompt: !continueMessage, + tools: mappedTools.length ? mappedTools : undefined, + }); + + if (typeof output !== "string") { + throw new Error("Failed to apply chat template, the output is not a string"); + } + + return output; + }; + return renderTemplate; +} + +const processModel = async (m: z.infer) => ({ + ...m, + chatPromptRender: await getChatPromptRender(m), + id: m.id || m.name, + displayName: m.displayName || m.name, + preprompt: m.prepromptUrl ? await fetch(m.prepromptUrl).then((r) => r.text()) : m.preprompt, + parameters: { ...m.parameters, stop_sequences: m.parameters?.stop }, +}); + +const addEndpoint = (m: Awaited>) => ({ + ...m, + getEndpoint: async (): Promise => { + if (!m.endpoints) { + return endpointTgi({ + type: "tgi", + url: `${env.HF_API_ROOT}/${m.name}`, + accessToken: env.HF_TOKEN ?? env.HF_ACCESS_TOKEN, + weight: 1, + model: m, + }); + } + const totalWeight = sum(m.endpoints.map((e) => e.weight)); + + let random = Math.random() * totalWeight; + + for (const endpoint of m.endpoints) { + if (random < endpoint.weight) { + const args = { ...endpoint, model: m }; + + switch (args.type) { + case "tgi": + return endpoints.tgi(args); + case "anthropic": + return endpoints.anthropic(args); + case "anthropic-vertex": + return endpoints.anthropicvertex(args); + case "bedrock": + return endpoints.bedrock(args); + case "aws": + return await endpoints.aws(args); + case "openai": + return await endpoints.openai(args); + case "llamacpp": + return endpoints.llamacpp(args); + case "ollama": + return endpoints.ollama(args); + case "vertex": + return await endpoints.vertex(args); + case "genai": + return await endpoints.genai(args); + case "cloudflare": + return await endpoints.cloudflare(args); + case "cohere": + return await endpoints.cohere(args); + case "langserve": + return await endpoints.langserve(args); + default: + // for legacy reason + return endpoints.tgi(args); + } + } + random -= endpoint.weight; + } + + throw new Error(`Failed to select endpoint`); + }, +}); + +const inferenceApiIds = isHuggingChat + ? await fetch( + "https://huggingface.co/api/models?pipeline_tag=text-generation&inference=warm&filter=conversational" + ) + .then((r) => r.json()) + .then((json) => json.map((r: { id: string }) => r.id)) + .catch((err) => { + logger.error(err, "Failed to fetch inference API ids"); + return []; + }) + : []; + +export const models = await Promise.all( + modelsRaw.map((e) => + processModel(e) + .then(addEndpoint) + .then(async (m) => ({ + ...m, + hasInferenceAPI: inferenceApiIds.includes(m.id ?? m.name), + })) + ) +); + +export type ProcessedModel = (typeof models)[number]; + +// super ugly but not sure how to make typescript happier +export const validModelIdSchema = z.enum(models.map((m) => m.id) as [string, ...string[]]); + +export const defaultModel = models[0]; + +// Models that have been deprecated +export const oldModels = env.OLD_MODELS + ? z + .array( + z.object({ + id: z.string().optional(), + name: z.string().min(1), + displayName: z.string().min(1).optional(), + transferTo: validModelIdSchema.optional(), + }) + ) + .parse(JSON5.parse(env.OLD_MODELS)) + .map((m) => ({ ...m, id: m.id || m.name, displayName: m.displayName || m.name })) + : []; + +export const validateModel = (_models: BackendModel[]) => { + // Zod enum function requires 2 parameters + return z.enum([_models[0].id, ..._models.slice(1).map((m) => m.id)]); +}; + +// if `TASK_MODEL` is string & name of a model in `MODELS`, then we use `MODELS[TASK_MODEL]`, else we try to parse `TASK_MODEL` as a model config itself + +export const smallModel = env.TASK_MODEL + ? ((models.find((m) => m.name === env.TASK_MODEL) || + (await processModel(modelConfig.parse(JSON5.parse(env.TASK_MODEL))).then((m) => + addEndpoint(m) + ))) ?? + defaultModel) + : defaultModel; + +export type BackendModel = Optional< + typeof defaultModel, + "preprompt" | "parameters" | "multimodal" | "unlisted" | "tools" | "hasInferenceAPI" +>; diff --git a/src/lib/server/sendSlack.ts b/src/lib/server/sendSlack.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b0d11da3ab766b6d09ac839c397d1709e822563 --- /dev/null +++ b/src/lib/server/sendSlack.ts @@ -0,0 +1,23 @@ +import { env } from "$env/dynamic/private"; +import { logger } from "$lib/server/logger"; + +export async function sendSlack(text: string) { + if (!env.WEBHOOK_URL_REPORT_ASSISTANT) { + logger.warn("WEBHOOK_URL_REPORT_ASSISTANT is not set, tried to send a slack message."); + return; + } + + const res = await fetch(env.WEBHOOK_URL_REPORT_ASSISTANT, { + method: "POST", + headers: { + "Content-type": "application/json", + }, + body: JSON.stringify({ + text, + }), + }); + + if (!res.ok) { + logger.error(`Webhook message failed. ${res.statusText} ${res.text}`); + } +} diff --git a/src/lib/server/sentenceSimilarity.ts b/src/lib/server/sentenceSimilarity.ts new file mode 100644 index 0000000000000000000000000000000000000000..a9cdbbeb8e16ce7c3474a5af250b9ae09afcef71 --- /dev/null +++ b/src/lib/server/sentenceSimilarity.ts @@ -0,0 +1,33 @@ +import { dot } from "@huggingface/transformers"; +import type { EmbeddingBackendModel } from "$lib/server/embeddingModels"; +import type { Embedding } from "$lib/server/embeddingEndpoints/embeddingEndpoints"; + +// see here: https://github.com/nmslib/hnswlib/blob/359b2ba87358224963986f709e593d799064ace6/README.md?plain=1#L34 +export function innerProduct(embeddingA: Embedding, embeddingB: Embedding) { + return 1.0 - dot(embeddingA, embeddingB); +} + +export async function getSentenceSimilarity( + embeddingModel: EmbeddingBackendModel, + query: string, + sentences: string[] +): Promise<{ distance: number; embedding: Embedding; idx: number }[]> { + const inputs = [ + `${embeddingModel.preQuery}${query}`, + ...sentences.map((sentence) => `${embeddingModel.prePassage}${sentence}`), + ]; + + const embeddingEndpoint = await embeddingModel.getEndpoint(); + const output = await embeddingEndpoint({ inputs }).catch((err) => { + throw Error("Failed to generate embeddings for sentence similarity", { cause: err }); + }); + + const queryEmbedding: Embedding = output[0]; + const sentencesEmbeddings: Embedding[] = output.slice(1); + + return sentencesEmbeddings.map((sentenceEmbedding, idx) => ({ + distance: innerProduct(queryEmbedding, sentenceEmbedding), + embedding: sentenceEmbedding, + idx, + })); +} diff --git a/src/lib/server/textGeneration/assistant.ts b/src/lib/server/textGeneration/assistant.ts new file mode 100644 index 0000000000000000000000000000000000000000..1410513c7928cd6117113c5970bcca0914e412c0 --- /dev/null +++ b/src/lib/server/textGeneration/assistant.ts @@ -0,0 +1,75 @@ +import { isURLLocal } from "../isURLLocal"; +import { env } from "$env/dynamic/private"; +import { collections } from "$lib/server/database"; +import type { Assistant } from "$lib/types/Assistant"; +import type { ObjectId } from "mongodb"; + +export async function processPreprompt(preprompt: string, user_message: string | undefined) { + // Replace {{today}} with formatted date + const today = new Intl.DateTimeFormat("en-US", { + weekday: "long", + day: "numeric", + month: "long", + year: "numeric", + }).format(new Date()); + preprompt = preprompt.replaceAll("{{today}}", today); + const requestRegex = /{{\s?(get|post|url)=(.*?)\s?}}/g; + + for (const match of preprompt.matchAll(requestRegex)) { + const method = match[1].toUpperCase(); + const urlString = match[2]; + try { + const url = new URL(urlString); + if ((await isURLLocal(url)) && env.ENABLE_LOCAL_FETCH !== "true") { + throw new Error("URL couldn't be fetched, it resolved to a local address."); + } + + let res; + if (method == "POST") { + res = await fetch(url.href, { + method: "POST", + body: user_message, + headers: { + "Content-Type": "text/plain", + }, + }); + } else if (method == "GET" || method == "URL") { + res = await fetch(url.href); + } else { + throw new Error("Invalid method " + method); + } + + if (!res.ok) { + throw new Error("URL couldn't be fetched, error " + res.status); + } + const text = await res.text(); + preprompt = preprompt.replaceAll(match[0], text); + } catch (e) { + preprompt = preprompt.replaceAll(match[0], (e as Error).message); + } + } + + return preprompt; +} + +export async function getAssistantById(id?: ObjectId) { + return collections.assistants + .findOne< + Pick + >({ _id: id }, { projection: { rag: 1, dynamicPrompt: 1, generateSettings: 1, tools: 1 } }) + .then((a) => a ?? undefined); +} + +export function assistantHasWebSearch(assistant?: Pick | null) { + return ( + env.ENABLE_ASSISTANTS_RAG === "true" && + !!assistant?.rag && + (assistant.rag.allowedLinks.length > 0 || + assistant.rag.allowedDomains.length > 0 || + assistant.rag.allowAllDomains) + ); +} + +export function assistantHasDynamicPrompt(assistant?: Pick) { + return env.ENABLE_ASSISTANTS_RAG === "true" && Boolean(assistant?.dynamicPrompt); +} diff --git a/src/lib/server/textGeneration/generate.ts b/src/lib/server/textGeneration/generate.ts new file mode 100644 index 0000000000000000000000000000000000000000..59574b32d67416fabf9ab915de8eeb0739c68ded --- /dev/null +++ b/src/lib/server/textGeneration/generate.ts @@ -0,0 +1,198 @@ +import type { ToolResult, Tool } from "$lib/types/Tool"; +import { + MessageReasoningUpdateType, + MessageUpdateType, + type MessageUpdate, +} from "$lib/types/MessageUpdate"; +import { AbortedGenerations } from "../abortedGenerations"; +import type { TextGenerationContext } from "./types"; +import type { EndpointMessage } from "../endpoints/endpoints"; +import { generateFromDefaultEndpoint } from "../generateFromDefaultEndpoint"; +import { generateSummaryOfReasoning } from "./reasoning"; +import { logger } from "../logger"; + +type GenerateContext = Omit & { messages: EndpointMessage[] }; + +export async function* generate( + { model, endpoint, conv, messages, assistant, isContinue, promptedAt }: GenerateContext, + toolResults: ToolResult[], + preprompt?: string, + tools?: Tool[] +): AsyncIterable { + // reasoning mode is false by default + let reasoning = false; + let reasoningBuffer = ""; + let lastReasoningUpdate = new Date(); + let status = ""; + const startTime = new Date(); + if ( + model.reasoning && + // if the beginToken is an empty string, the model starts in reasoning mode + (model.reasoning.type === "regex" || + model.reasoning.type === "summarize" || + (model.reasoning.type === "tokens" && model.reasoning.beginToken === "")) + ) { + // if the model has reasoning in regex or summarize mode, it starts in reasoning mode + // and we extract the answer from the reasoning + reasoning = true; + yield { + type: MessageUpdateType.Reasoning, + subtype: MessageReasoningUpdateType.Status, + status: "Started reasoning...", + }; + } + + for await (const output of await endpoint({ + messages, + preprompt, + continueMessage: isContinue, + generateSettings: assistant?.generateSettings, + tools, + toolResults, + isMultimodal: model.multimodal, + conversationId: conv._id, + })) { + // text generation completed + if (output.generated_text) { + let interrupted = + !output.token.special && !model.parameters.stop?.includes(output.token.text); + + let text = output.generated_text.trimEnd(); + for (const stopToken of model.parameters.stop ?? []) { + if (!text.endsWith(stopToken)) continue; + + interrupted = false; + text = text.slice(0, text.length - stopToken.length); + } + + let finalAnswer = text; + if (model.reasoning && model.reasoning.type === "regex") { + const regex = new RegExp(model.reasoning.regex); + finalAnswer = regex.exec(reasoningBuffer)?.[1] ?? text; + } else if (model.reasoning && model.reasoning.type === "summarize") { + yield { + type: MessageUpdateType.Reasoning, + subtype: MessageReasoningUpdateType.Status, + status: "Summarizing reasoning...", + }; + try { + const summary = yield* generateFromDefaultEndpoint({ + messages: [ + { + from: "user", + content: `Question: ${ + messages[messages.length - 1].content + }\n\nReasoning: ${reasoningBuffer}`, + }, + ], + preprompt: `Your task is to summarize concisely all your reasoning steps and then give the final answer. Keep it short, one short paragraph at most. If the reasoning steps explicitly include a code solution, make sure to include it in your answer. + +If the user is just having a casual conversation that doesn't require explanations, answer directly without explaining your steps, otherwise make sure to summarize step by step, make sure to skip dead-ends in your reasoning and removing excess detail. + +Do not use prefixes such as Response: or Answer: when answering to the user.`, + generateSettings: { + max_new_tokens: 1024, + }, + }); + finalAnswer = summary; + yield { + type: MessageUpdateType.Reasoning, + subtype: MessageReasoningUpdateType.Status, + status: `Done in ${Math.round((new Date().getTime() - startTime.getTime()) / 1000)}s.`, + }; + } catch (e) { + finalAnswer = text; + logger.error(e); + } + } else if (model.reasoning && model.reasoning.type === "tokens") { + // make sure to remove the content of the reasoning buffer from + // the final answer to avoid duplication + + // if the beginToken is an empty string, we don't need to remove anything + const beginIndex = model.reasoning.beginToken + ? reasoningBuffer.indexOf(model.reasoning.beginToken) + : 0; + const endIndex = reasoningBuffer.lastIndexOf(model.reasoning.endToken); + + if (beginIndex !== -1 && endIndex !== -1) { + // Remove the reasoning section (including tokens) from final answer + finalAnswer = + text.slice(0, beginIndex) + text.slice(endIndex + model.reasoning.endToken.length); + } + } + + yield { + type: MessageUpdateType.FinalAnswer, + text: finalAnswer, + interrupted, + webSources: output.webSources, + }; + continue; + } + + if (model.reasoning && model.reasoning.type === "tokens") { + if (output.token.text === model.reasoning.beginToken) { + reasoning = true; + reasoningBuffer += output.token.text; + yield { + type: MessageUpdateType.Reasoning, + subtype: MessageReasoningUpdateType.Status, + status: "Started thinking...", + }; + continue; + } else if (output.token.text === model.reasoning.endToken) { + reasoning = false; + reasoningBuffer += output.token.text; + yield { + type: MessageUpdateType.Reasoning, + subtype: MessageReasoningUpdateType.Status, + status: `Done in ${Math.round((new Date().getTime() - startTime.getTime()) / 1000)}s.`, + }; + continue; + } + } + // ignore special tokens + if (output.token.special) continue; + + // pass down normal token + if (reasoning) { + reasoningBuffer += output.token.text; + + // yield status update if it has changed + if (status !== "") { + yield { + type: MessageUpdateType.Reasoning, + subtype: MessageReasoningUpdateType.Status, + status, + }; + status = ""; + } + + // create a new status every 5 seconds + if (new Date().getTime() - lastReasoningUpdate.getTime() > 4000) { + lastReasoningUpdate = new Date(); + try { + generateSummaryOfReasoning(reasoningBuffer).then((summary) => { + status = summary; + }); + } catch (e) { + logger.error(e); + } + } + yield { + type: MessageUpdateType.Reasoning, + subtype: MessageReasoningUpdateType.Stream, + token: output.token.text, + }; + } else { + yield { type: MessageUpdateType.Stream, token: output.token.text }; + } + + // abort check + const date = AbortedGenerations.getInstance().getList().get(conv._id.toString()); + if (date && date > promptedAt) break; + + // no output check + if (!output) break; + } +} diff --git a/src/lib/server/textGeneration/index.ts b/src/lib/server/textGeneration/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0142acfbb528490e33d8917356d4f47ad183fdfa --- /dev/null +++ b/src/lib/server/textGeneration/index.ts @@ -0,0 +1,89 @@ +import { runWebSearch } from "$lib/server/websearch/runWebSearch"; +import { preprocessMessages } from "../endpoints/preprocessMessages"; + +import { generateTitleForConversation } from "./title"; +import { + assistantHasDynamicPrompt, + assistantHasWebSearch, + getAssistantById, + processPreprompt, +} from "./assistant"; +import { getTools, runTools } from "./tools"; +import type { WebSearch } from "$lib/types/WebSearch"; +import { + type MessageUpdate, + MessageUpdateType, + MessageUpdateStatus, +} from "$lib/types/MessageUpdate"; +import { generate } from "./generate"; +import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators"; +import type { TextGenerationContext } from "./types"; +import type { ToolResult } from "$lib/types/Tool"; +import { toolHasName } from "../tools/utils"; +import directlyAnswer from "../tools/directlyAnswer"; + +async function* keepAlive(done: AbortSignal): AsyncGenerator { + while (!done.aborted) { + yield { + type: MessageUpdateType.Status, + status: MessageUpdateStatus.KeepAlive, + }; + await new Promise((resolve) => setTimeout(resolve, 100)); + } +} + +export async function* textGeneration(ctx: TextGenerationContext) { + const done = new AbortController(); + + const titleGen = generateTitleForConversation(ctx.conv); + const textGen = textGenerationWithoutTitle(ctx, done); + const keepAliveGen = keepAlive(done.signal); + + // keep alive until textGen is done + + yield* mergeAsyncGenerators([titleGen, textGen, keepAliveGen]); +} + +async function* textGenerationWithoutTitle( + ctx: TextGenerationContext, + done: AbortController +): AsyncGenerator { + yield { + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Started, + }; + + ctx.assistant ??= await getAssistantById(ctx.conv.assistantId); + const { model, conv, messages, assistant, isContinue, webSearch, toolsPreference } = ctx; + const convId = conv._id; + + let webSearchResult: WebSearch | undefined; + + // run websearch if: + // - it's not continuing a previous message + // - AND the model doesn't support tools and websearch is selected + // - OR the assistant has websearch enabled (no tools for assistants for now) + if (!isContinue && ((webSearch && !conv.assistantId) || assistantHasWebSearch(assistant))) { + webSearchResult = yield* runWebSearch(conv, messages, assistant?.rag); + } + + let preprompt = conv.preprompt; + if (assistantHasDynamicPrompt(assistant) && preprompt) { + preprompt = await processPreprompt(preprompt, messages.at(-1)?.content); + if (messages[0].from === "system") messages[0].content = preprompt; + } + + let toolResults: ToolResult[] = []; + let tools = model.tools ? await getTools(toolsPreference, ctx.assistant) : undefined; + + if (tools) { + const toolCallsRequired = tools.some((tool) => !toolHasName(directlyAnswer.name, tool)); + if (toolCallsRequired) { + toolResults = yield* runTools(ctx, tools, preprompt); + } else tools = undefined; + } + + const processedMessages = await preprocessMessages(messages, webSearchResult, convId); + yield* generate({ ...ctx, messages: processedMessages }, toolResults, preprompt); + done.abort(); +} diff --git a/src/lib/server/textGeneration/reasoning.ts b/src/lib/server/textGeneration/reasoning.ts new file mode 100644 index 0000000000000000000000000000000000000000..061aad628611b2a36bc7ad91292037e76f99f3e9 --- /dev/null +++ b/src/lib/server/textGeneration/reasoning.ts @@ -0,0 +1,73 @@ +import { generateFromDefaultEndpoint } from "../generateFromDefaultEndpoint"; +import { smallModel } from "../models"; +import { getReturnFromGenerator } from "$lib/utils/getReturnFromGenerator"; +import { getToolOutput } from "../tools/getToolOutput"; +import type { Tool } from "$lib/types/Tool"; +import { logger } from "../logger"; + +export async function generateSummaryOfReasoning(buffer: string): Promise { + let summary: string | undefined; + + const messages = [ + { + from: "user" as const, + content: buffer.slice(-300), + }, + ]; + + const preprompt = `You are tasked with submitting a summary of the latest reasoning steps into a tool. Never describe results of the reasoning, only the process. Remain vague in your summary. +The text might be incomplete, try your best to summarize it in one very short sentence, starting with a gerund and ending with three points. The sentence must be very short, ideally 5 words or less.`; + + if (smallModel.tools) { + const summaryTool = { + name: "summary", + description: "Submit a summary for the submitted text", + inputs: [ + { + name: "summary", + type: "str", + description: + "The short summary of the reasoning steps. 5 words or less. Must start with a gerund.", + paramType: "required", + }, + ], + } as unknown as Tool; + + const endpoint = await smallModel.getEndpoint(); + summary = await getToolOutput({ + messages, + preprompt, + tool: summaryTool, + endpoint, + }).catch(() => { + logger.warn("Error getting tool output"); + return undefined; + }); + } + + if (!summary) { + summary = await getReturnFromGenerator( + generateFromDefaultEndpoint({ + messages: [ + { + from: "user", + content: buffer.slice(-300), + }, + ], + preprompt: `You are tasked with summarizing the latest reasoning steps. Never describe results of the reasoning, only the process. Remain vague in your summary. + The text might be incomplete, try your best to summarize it in one very short sentence, starting with a gerund and ending with three points. + Example: "Thinking about life...", "Summarizing the results...", "Processing the input..."`, + generateSettings: { + max_new_tokens: 50, + }, + }) + ); + } + + if (!summary) { + return "Reasoning..."; + } + + const parts = summary.split("..."); + return parts[0].slice(0, 100) + "..."; +} diff --git a/src/lib/server/textGeneration/title.ts b/src/lib/server/textGeneration/title.ts new file mode 100644 index 0000000000000000000000000000000000000000..a241d37401f9f05800d88ce498037c122cdd5ca4 --- /dev/null +++ b/src/lib/server/textGeneration/title.ts @@ -0,0 +1,94 @@ +import { env } from "$env/dynamic/private"; +import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint"; +import { logger } from "$lib/server/logger"; +import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate"; +import type { Conversation } from "$lib/types/Conversation"; +import { getReturnFromGenerator } from "$lib/utils/getReturnFromGenerator"; +import { smallModel } from "../models"; +import type { Tool } from "$lib/types/Tool"; +import { getToolOutput } from "../tools/getToolOutput"; + +export async function* generateTitleForConversation( + conv: Conversation +): AsyncGenerator { + try { + const userMessage = conv.messages.find((m) => m.from === "user"); + // HACK: detect if the conversation is new + if (conv.title !== "New Chat" || !userMessage) return; + + const prompt = userMessage.content; + const title = (await generateTitle(prompt)) ?? "New Chat"; + + yield { + type: MessageUpdateType.Title, + title, + }; + } catch (cause) { + logger.error(Error("Failed whilte generating title for conversation", { cause })); + } +} + +export async function generateTitle(prompt: string) { + if (env.LLM_SUMMARIZATION !== "true") { + return prompt.split(/\s+/g).slice(0, 5).join(" "); + } + + if (smallModel.tools) { + const titleTool = { + name: "title", + description: + "Submit a title for the conversation so far. Do not try to answer the user question or the tool will fail.", + inputs: [ + { + name: "title", + type: "str", + description: + "The title for the conversation. It should be 5 words or less and start with a unicode emoji relevant to the query.", + }, + ], + } as unknown as Tool; + + const endpoint = await smallModel.getEndpoint(); + const title = await getToolOutput({ + messages: [ + { + from: "user" as const, + content: prompt, + }, + ], + preprompt: + "The task is to generate conversation titles based on text snippets. You'll never answer the provided question directly, but instead summarize the user's request into a short title.", + tool: titleTool, + endpoint, + }); + + if (title) { + if (!/\p{Emoji}/u.test(title.slice(0, 3))) { + return "💬 " + title; + } + return title; + } + } + + return await getReturnFromGenerator( + generateFromDefaultEndpoint({ + messages: [{ from: "user", content: prompt }], + preprompt: + "You are a summarization AI. Summarize the user's request into a single short sentence of four words or less. Do not try to answer it, only summarize the user's query. Always start your answer with an emoji relevant to the summary", + generateSettings: { + max_new_tokens: 30, + }, + }) + ) + .then((summary) => { + // add an emoji if none is found in the first three characters + if (!/\p{Emoji}/u.test(summary.slice(0, 3))) { + return "💬 " + summary; + } + return summary; + }) + .catch((e) => { + logger.error(e); + return null; + }); +} diff --git a/src/lib/server/textGeneration/tools.ts b/src/lib/server/textGeneration/tools.ts new file mode 100644 index 0000000000000000000000000000000000000000..2fb045772105ab7d83580ad60bcfac261a2408a4 --- /dev/null +++ b/src/lib/server/textGeneration/tools.ts @@ -0,0 +1,359 @@ +import { ToolResultStatus, type ToolCall, type Tool, type ToolResult } from "$lib/types/Tool"; +import { v4 as uuidV4 } from "uuid"; +import { getCallMethod, toolFromConfigs, type BackendToolContext } from "../tools"; +import { + MessageToolUpdateType, + MessageUpdateStatus, + MessageUpdateType, + type MessageUpdate, +} from "$lib/types/MessageUpdate"; +import type { TextGenerationContext } from "./types"; + +import directlyAnswer from "../tools/directlyAnswer"; +import websearch from "../tools/web/search"; +import { z } from "zod"; +import { logger } from "../logger"; +import { extractJson, toolHasName } from "../tools/utils"; +import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators"; +import { MetricsServer } from "../metrics"; +import { stringifyError } from "$lib/utils/stringifyError"; +import { collections } from "../database"; +import { ObjectId } from "mongodb"; +import type { Message } from "$lib/types/Message"; +import type { Assistant } from "$lib/types/Assistant"; +import { assistantHasWebSearch } from "./assistant"; + +export async function getTools( + toolsPreference: Array, + assistant: Pick | undefined +): Promise { + let preferences = toolsPreference; + + if (assistant) { + if (assistant?.tools?.length) { + preferences = assistant.tools; + + if (assistantHasWebSearch(assistant)) { + preferences.push(websearch._id.toString()); + } + } else { + if (assistantHasWebSearch(assistant)) { + return [websearch, directlyAnswer]; + } + return [directlyAnswer]; + } + } + + // filter based on tool preferences, add the tools that are on by default + const activeConfigTools = toolFromConfigs.filter((el) => { + if (el.isLocked && el.isOnByDefault && !assistant) return true; + return preferences?.includes(el._id.toString()) ?? (el.isOnByDefault && !assistant); + }); + + // find tool where the id is in preferences + const activeCommunityTools = await collections.tools + .find({ + _id: { $in: preferences.map((el) => new ObjectId(el)) }, + }) + .toArray() + .then((el) => el.map((el) => ({ ...el, call: getCallMethod(el) }))); + + return [...activeConfigTools, ...activeCommunityTools]; +} + +async function* callTool( + ctx: BackendToolContext, + tools: Tool[], + call: ToolCall +): AsyncGenerator { + const uuid = uuidV4(); + + const tool = tools.find((el) => toolHasName(call.name, el)); + if (!tool) { + return { call, status: ToolResultStatus.Error, message: `Could not find tool "${call.name}"` }; + } + + // Special case for directly_answer tool where we ignore + if (toolHasName(directlyAnswer.name, tool)) return; + + const startTime = Date.now(); + MetricsServer.getMetrics().tool.toolUseCount.inc({ tool: call.name }); + + yield { + type: MessageUpdateType.Tool, + subtype: MessageToolUpdateType.Call, + uuid, + call, + }; + + try { + const toolResult = yield* tool.call(call.parameters, ctx, uuid); + + yield { + type: MessageUpdateType.Tool, + subtype: MessageToolUpdateType.Result, + uuid, + result: { ...toolResult, call, status: ToolResultStatus.Success }, + }; + + MetricsServer.getMetrics().tool.toolUseDuration.observe( + { tool: call.name }, + Date.now() - startTime + ); + + await collections.tools.findOneAndUpdate({ _id: tool._id }, { $inc: { useCount: 1 } }); + + return { ...toolResult, call, status: ToolResultStatus.Success }; + } catch (error) { + MetricsServer.getMetrics().tool.toolUseCountError.inc({ tool: call.name }); + logger.error(error, `Failed while running tool ${call.name}. ${stringifyError(error)}`); + + yield { + type: MessageUpdateType.Tool, + subtype: MessageToolUpdateType.Error, + uuid, + message: + "An error occurred while calling the tool " + call.name + ": " + stringifyError(error), + }; + + return { + call, + status: ToolResultStatus.Error, + message: + "An error occurred while calling the tool " + call.name + ": " + stringifyError(error), + }; + } +} + +export async function* runTools( + ctx: TextGenerationContext, + tools: Tool[], + preprompt?: string +): AsyncGenerator { + const { endpoint, conv, messages, assistant, ip, username } = ctx; + const calls: ToolCall[] = []; + + const pickToolStartTime = Date.now(); + // append a message with the list of all available files + + const files = messages.reduce((acc, curr, idx) => { + if (curr.files) { + const prefix = (curr.from === "user" ? "input" : "ouput") + "_" + idx; + acc.push( + ...curr.files.map( + (file, fileIdx) => `${prefix}_${fileIdx}.${file?.name?.split(".")?.pop()?.toLowerCase()}` + ) + ); + } + return acc; + }, [] as string[]); + + let formattedMessages = messages.map((message, msgIdx) => { + let content = message.content; + + if (message.files && message.files.length > 0) { + content += + "\n\nAdded files: \n - " + + message.files + .map((file, fileIdx) => { + const prefix = message.from === "user" ? "input" : "output"; + const fileName = file.name.split(".").pop()?.toLowerCase(); + + return `${prefix}_${msgIdx}_${fileIdx}.${fileName}`; + }) + .join("\n - "); + } + + return { + ...message, + content, + } satisfies Message; + }); + + const fileMsg = { + id: crypto.randomUUID(), + from: "system", + content: + "Here is the list of available filenames that can be used as input for tools. Use the filenames that are in this list. \n The filename structure is as follows : {input for user|output for tool}_{message index in the conversation}_{file index in the list of files}.{file extension} \n - " + + files.join("\n - ") + + "\n\n\n", + } satisfies Message; + + // put fileMsg before last if files.length > 0 + formattedMessages = files.length + ? [...formattedMessages.slice(0, -1), fileMsg, ...formattedMessages.slice(-1)] + : messages; + + let rawText = ""; + + const mappedTools = tools.map((tool) => ({ + ...tool, + inputs: tool.inputs.map((input) => ({ + ...input, + type: input.type === "file" ? "str" : input.type, + })), + })); + + // do the function calling bits here + for await (const output of await endpoint({ + messages: formattedMessages, + preprompt, + generateSettings: { temperature: 0.1, ...assistant?.generateSettings }, + tools: mappedTools, + conversationId: conv._id, + })) { + // model natively supports tool calls + if (output.token.toolCalls) { + calls.push(...output.token.toolCalls); + continue; + } + + if (output.token.text) { + rawText += output.token.text; + } + + // if we dont see a tool call in the first 25 chars, something is going wrong and we abort + if (rawText.length > 100 && !(rawText.includes("```json") || rawText.includes("{"))) { + return []; + } + + // if we see a directly_answer tool call, we skip the rest + if ( + rawText.includes("directly_answer") || + rawText.includes("directlyAnswer") || + rawText.includes("directly-answer") + ) { + return []; + } + + // look for a code blocks of ```json and parse them + // if they're valid json, add them to the calls array + if (output.generated_text) { + try { + const rawCalls = await extractJson(output.generated_text); + const newCalls = rawCalls + .map((call) => externalToToolCall(call, tools)) + .filter((call) => call !== undefined) as ToolCall[]; + + calls.push(...newCalls); + } catch (e) { + logger.warn({ rawCall: output.generated_text, error: e }, "Error while parsing tool calls"); + // error parsing the calls + yield { + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Error, + message: "Error while parsing tool calls.", + }; + } + } + } + + MetricsServer.getMetrics().tool.timeToChooseTools.observe( + { model: conv.model }, + Date.now() - pickToolStartTime + ); + + const toolContext: BackendToolContext = { conv, messages, preprompt, assistant, ip, username }; + const toolResults: (ToolResult | undefined)[] = yield* mergeAsyncGenerators( + calls.map((call) => callTool(toolContext, tools, call)) + ); + return toolResults.filter((result): result is ToolResult => result !== undefined); +} + +export function externalToToolCall(call: unknown, tools: Tool[]): ToolCall | undefined { + // Early return if invalid input + if (!isValidCallObject(call)) { + return undefined; + } + + const parsedCall = parseExternalCall(call); + if (!parsedCall) return undefined; + + const tool = tools.find((tool) => toolHasName(parsedCall.tool_name, tool)); + if (!tool) { + logger.debug( + `Model requested tool that does not exist: "${parsedCall.tool_name}". Skipping tool...` + ); + return undefined; + } + + const parametersWithDefaults: Record = {}; + + for (const input of tool.inputs) { + const value = parsedCall.parameters[input.name]; + + // Required so ensure it's there, otherwise return undefined + if (input.paramType === "required") { + if (value === undefined) { + logger.debug( + `Model requested tool "${parsedCall.tool_name}" but was missing required parameter "${input.name}". Skipping tool...` + ); + return; + } + parametersWithDefaults[input.name] = value; + continue; + } + + // Optional so use default if not there + parametersWithDefaults[input.name] = value; + + if (input.paramType === "optional") { + parametersWithDefaults[input.name] ??= input.default.toString(); + } + } + + return { + name: parsedCall.tool_name, + parameters: parametersWithDefaults, + }; +} + +// Helper functions +function isValidCallObject(call: unknown): call is Record { + return typeof call === "object" && call !== null; +} + +function parseExternalCall(callObj: Record) { + let toolCall = callObj; + if ( + isValidCallObject(callObj) && + "function" in callObj && + isValidCallObject(callObj.function) && + "_name" in callObj.function + ) { + toolCall = { + tool_name: callObj["function"]["_name"], + parameters: { + ...callObj["function"], + _name: undefined, + }, + }; + } + + const nameFields = ["tool_name", "name"] as const; + const parametersFields = ["parameters", "arguments", "parameter_definitions"] as const; + + const groupedCall = { + tool_name: "" as string, + parameters: undefined as Record | undefined, + }; + + for (const name of nameFields) { + if (toolCall[name]) { + groupedCall.tool_name = toolCall[name] as string; + } + } + + for (const name of parametersFields) { + if (toolCall[name]) { + groupedCall.parameters = toolCall[name] as Record; + } + } + + return z + .object({ + tool_name: z.string(), + parameters: z.record(z.any()), + }) + .parse(groupedCall); +} diff --git a/src/lib/server/textGeneration/types.ts b/src/lib/server/textGeneration/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..05df23cf520d748a4aaa4229ce60ddac2726b0e3 --- /dev/null +++ b/src/lib/server/textGeneration/types.ts @@ -0,0 +1,19 @@ +import type { ProcessedModel } from "../models"; +import type { Endpoint } from "../endpoints/endpoints"; +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import type { Assistant } from "$lib/types/Assistant"; + +export interface TextGenerationContext { + model: ProcessedModel; + endpoint: Endpoint; + conv: Conversation; + messages: Message[]; + assistant?: Pick; + isContinue: boolean; + webSearch: boolean; + toolsPreference: Array; + promptedAt: Date; + ip: string; + username?: string; +} diff --git a/src/lib/server/tools/calculator.ts b/src/lib/server/tools/calculator.ts new file mode 100644 index 0000000000000000000000000000000000000000..cf52da237586258a6011b14e976e0af9eb765d1f --- /dev/null +++ b/src/lib/server/tools/calculator.ts @@ -0,0 +1,40 @@ +import type { ConfigTool } from "$lib/types/Tool"; +import { ObjectId } from "mongodb"; +import vm from "node:vm"; + +const calculator: ConfigTool = { + _id: new ObjectId("00000000000000000000000C"), + type: "config", + description: "Calculate the result of a mathematical expression", + color: "blue", + icon: "code", + displayName: "Calculator", + name: "calculator", + endpoint: null, + inputs: [ + { + name: "equation", + type: "str", + description: + "A mathematical expression to be evaluated. The result of the expression will be returned.", + paramType: "required", + }, + ], + outputComponent: null, + outputComponentIdx: null, + showOutput: false, + async *call({ equation }) { + try { + const blocks = String(equation).split("\n"); + const query = blocks[blocks.length - 1].replace(/[^-()\d/*+.]/g, ""); + + return { + outputs: [{ calculator: `${query} = ${vm.runInNewContext(query)}` }], + }; + } catch (cause) { + throw new Error("Invalid expression", { cause }); + } + }, +}; + +export default calculator; diff --git a/src/lib/server/tools/directlyAnswer.ts b/src/lib/server/tools/directlyAnswer.ts new file mode 100644 index 0000000000000000000000000000000000000000..f582bec4e2d3bed61f2208b7be5ef6d7828d0f34 --- /dev/null +++ b/src/lib/server/tools/directlyAnswer.ts @@ -0,0 +1,28 @@ +import type { ConfigTool } from "$lib/types/Tool"; +import { ObjectId } from "mongodb"; + +const directlyAnswer: ConfigTool = { + _id: new ObjectId("00000000000000000000000D"), + type: "config", + description: "Answer the user's query directly", + color: "blue", + icon: "chat", + displayName: "Directly Answer", + isOnByDefault: true, + isLocked: true, + isHidden: true, + name: "directlyAnswer", + endpoint: null, + inputs: [], + outputComponent: null, + outputComponentIdx: null, + showOutput: false, + async *call() { + return { + outputs: [], + display: false, + }; + }, +}; + +export default directlyAnswer; diff --git a/src/lib/server/tools/getToolOutput.ts b/src/lib/server/tools/getToolOutput.ts new file mode 100644 index 0000000000000000000000000000000000000000..adaa012f4bcc088a7b931ca33108a167070c7f15 --- /dev/null +++ b/src/lib/server/tools/getToolOutput.ts @@ -0,0 +1,70 @@ +import type { Tool } from "$lib/types/Tool"; +import { extractJson } from "./utils"; +import { externalToToolCall } from "../textGeneration/tools"; +import { logger } from "../logger"; +import type { Endpoint, EndpointMessage } from "../endpoints/endpoints"; + +interface GetToolOutputOptions { + messages: EndpointMessage[]; + tool: Tool; + preprompt?: string; + endpoint: Endpoint; + generateSettings?: { + max_new_tokens?: number; + [key: string]: unknown; + }; +} + +export async function getToolOutput({ + messages, + preprompt, + tool, + endpoint, + generateSettings = { max_new_tokens: 64 }, +}: GetToolOutputOptions): Promise { + try { + const stream = await endpoint({ + messages, + preprompt: preprompt + `\n\n Only use tool ${tool.name}.`, + tools: [tool], + generateSettings, + }); + + const calls = []; + + for await (const output of stream) { + if (output.token.toolCalls) { + calls.push(...output.token.toolCalls); + } + if (output.generated_text) { + const extractedCalls = await extractJson(output.generated_text).then((calls) => + calls.map((call) => externalToToolCall(call, [tool])).filter((call) => call !== undefined) + ); + calls.push(...extractedCalls); + } + + if (calls.length > 0) { + break; + } + } + + if (calls.length > 0) { + // Find the tool call matching our tool + const toolCall = calls.find((call) => call.name === tool.name); + + // If we found a matching call and it has parameters + if (toolCall?.parameters) { + // Get the first parameter value since most tools have a single main parameter + const firstParamValue = Object.values(toolCall.parameters)[0]; + if (typeof firstParamValue === "string") { + return firstParamValue as T; + } + } + } + + return undefined; + } catch (error) { + logger.warn(error, "Error getting tool output"); + return undefined; + } +} diff --git a/src/lib/server/tools/index.ts b/src/lib/server/tools/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d3e312e3fbbdb42fd5463b44a081b69b5c681c4 --- /dev/null +++ b/src/lib/server/tools/index.ts @@ -0,0 +1,309 @@ +import { MessageUpdateType } from "$lib/types/MessageUpdate"; +import { + ToolColor, + ToolIcon, + ToolOutputComponents, + type BackendCall, + type BaseTool, + type ConfigTool, + type ToolInput, +} from "$lib/types/Tool"; +import type { TextGenerationContext } from "../textGeneration/types"; + +import { z } from "zod"; +import JSON5 from "json5"; +import { env } from "$env/dynamic/private"; + +import jp from "jsonpath"; +import calculator from "./calculator"; +import directlyAnswer from "./directlyAnswer"; +import fetchUrl from "./web/url"; +import websearch from "./web/search"; +import { callSpace, getIpToken } from "./utils"; +import { uploadFile } from "../files/uploadFile"; +import type { MessageFile } from "$lib/types/Message"; +import { sha256 } from "$lib/utils/sha256"; +import { ObjectId } from "mongodb"; +import { isValidOutputComponent, ToolOutputPaths } from "./outputs"; +import { downloadFile } from "../files/downloadFile"; +import { fileTypeFromBlob } from "file-type"; + +export type BackendToolContext = Pick< + TextGenerationContext, + "conv" | "messages" | "assistant" | "ip" | "username" +> & { preprompt?: string }; + +const IOType = z.union([z.literal("str"), z.literal("int"), z.literal("float"), z.literal("bool")]); + +const toolInputBaseSchema = z.union([ + z.object({ + name: z.string().min(1).max(80), + description: z.string().max(200).optional(), + paramType: z.literal("required"), + }), + z.object({ + name: z.string().min(1).max(80), + description: z.string().max(200).optional(), + paramType: z.literal("optional"), + default: z + .union([z.string().max(300), z.number(), z.boolean(), z.undefined()]) + .transform((val) => (val === undefined ? "" : val)), + }), + z.object({ + name: z.string().min(1).max(80), + paramType: z.literal("fixed"), + value: z + .union([z.string().max(300), z.number(), z.boolean(), z.undefined()]) + .transform((val) => (val === undefined ? "" : val)), + }), +]); + +const toolInputSchema = toolInputBaseSchema.and( + z.object({ type: IOType }).or( + z.object({ + type: z.literal("file"), + mimeTypes: z.string().min(1), + }) + ) +); + +export const editableToolSchema = z + .object({ + name: z + .string() + .regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/) // only allow letters, numbers, and underscores, and start with a letter or underscore + .min(1) + .max(40), + // only allow huggingface spaces either through namespace or direct URLs + baseUrl: z.union([ + z.string().regex(/^[^/]+\/[^/]+$/), + z + .string() + .regex(/^https:\/\/huggingface\.co\/spaces\/[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+$/) + .transform((url) => url.split("/").slice(-2).join("/")), + ]), + endpoint: z.string().min(1).max(100), + inputs: z.array(toolInputSchema), + outputComponent: z.string().min(1).max(100), + showOutput: z.boolean(), + displayName: z.string().min(1).max(40), + color: ToolColor, + icon: ToolIcon, + description: z.string().min(1).max(100), + }) + .transform((tool) => ({ + ...tool, + outputComponentIdx: parseInt(tool.outputComponent.split(";")[0]), + outputComponent: ToolOutputComponents.parse(tool.outputComponent.split(";")[1]), + })); +export const configTools = z + .array( + z + .object({ + name: z.string(), + description: z.string(), + endpoint: z.union([z.string(), z.null()]), + inputs: z.array(toolInputSchema), + outputComponent: ToolOutputComponents.or(z.null()), + outputComponentIdx: z.number().int().default(0), + showOutput: z.boolean(), + _id: z + .string() + .length(24) + .regex(/^[0-9a-fA-F]{24}$/) + .transform((val) => new ObjectId(val)), + baseUrl: z.string().optional(), + displayName: z.string(), + color: ToolColor, + icon: ToolIcon, + isOnByDefault: z.optional(z.literal(true)), + isLocked: z.optional(z.literal(true)), + isHidden: z.optional(z.literal(true)), + }) + .transform((val) => ({ + type: "config" as const, + ...val, + call: getCallMethod(val), + })) + ) + // add the extra hardcoded tools + .transform((val) => [...val, calculator, directlyAnswer, fetchUrl, websearch]); + +export function getCallMethod(tool: Omit): BackendCall { + return async function* (params, ctx, uuid) { + if ( + tool.endpoint === null || + !tool.baseUrl || + !tool.outputComponent || + tool.outputComponentIdx === null + ) { + throw new Error(`Tool function ${tool.name} has no endpoint`); + } + + const ipToken = await getIpToken(ctx.ip, ctx.username); + + function coerceInput(value: unknown, type: ToolInput["type"]) { + const valueStr = String(value); + switch (type) { + case "str": + return valueStr; + case "int": + return parseInt(valueStr); + case "float": + return parseFloat(valueStr); + case "bool": + return valueStr === "true"; + default: + throw new Error(`Unsupported type ${type}`); + } + } + const inputs = tool.inputs.map(async (input) => { + if (input.type === "file" && input.paramType !== "required") { + throw new Error("File inputs are always required and cannot be optional or fixed"); + } + + if (input.paramType === "fixed") { + return coerceInput(input.value, input.type); + } else if (input.paramType === "optional") { + return coerceInput(params[input.name] ?? input.default, input.type); + } else if (input.paramType === "required") { + if (params[input.name] === undefined) { + throw new Error(`Missing required input ${input.name}`); + } + + if (input.type === "file") { + // todo: parse file here ! + // structure is {input|output}-{msgIdx}-{fileIdx}-{filename} + + const filename = params[input.name]; + + if (!filename || typeof filename !== "string") { + throw new Error(`Filename is not a string`); + } + + const messages = ctx.messages; + + const msgIdx = parseInt(filename.split("_")[1]); + const fileIdx = parseInt(filename.split("_")[2]); + + if (Number.isNaN(msgIdx) || Number.isNaN(fileIdx)) { + throw Error(`Message index or file index is missing`); + } + + if (msgIdx >= messages.length) { + throw Error(`Message index ${msgIdx} is out of bounds`); + } + + const file = messages[msgIdx].files?.[fileIdx]; + + if (!file) { + throw Error(`File index ${fileIdx} is out of bounds`); + } + + const blob = await downloadFile(file.value, ctx.conv._id) + .then((file) => fetch(`data:${file.mime};base64,${file.value}`)) + .then((res) => res.blob()) + .catch((err) => { + throw Error("Failed to download file", { cause: err }); + }); + + return blob; + } else { + return coerceInput(params[input.name], input.type); + } + } + }); + + const outputs = yield* callSpace( + tool.baseUrl, + tool.endpoint, + await Promise.all(inputs), + ipToken, + uuid + ); + + if (!isValidOutputComponent(tool.outputComponent)) { + throw new Error(`Tool output component is not defined`); + } + + const { type, path } = ToolOutputPaths[tool.outputComponent]; + + if (!path || !type) { + throw new Error(`Tool output type ${tool.outputComponent} is not supported`); + } + + const files: MessageFile[] = []; + + const toolOutputs: Array> = []; + + if (outputs.length <= tool.outputComponentIdx) { + throw new Error(`Tool output component index is out of bounds`); + } + + // if its not an object, return directly + if ( + outputs[tool.outputComponentIdx] !== undefined && + typeof outputs[tool.outputComponentIdx] !== "object" + ) { + return { + outputs: [{ [tool.name + "-0"]: outputs[tool.outputComponentIdx] }], + display: tool.showOutput, + }; + } + + await Promise.all( + jp + .query(outputs[tool.outputComponentIdx], path) + .map(async (output: string | string[], idx) => { + const arrayedOutput = Array.isArray(output) ? output : [output]; + if (type === "file") { + // output files are actually URLs + + await Promise.all( + arrayedOutput.map(async (output, idx) => { + await fetch(output) + .then((res) => res.blob()) + .then(async (blob) => { + const { ext, mime } = (await fileTypeFromBlob(blob)) ?? { ext: "octet-stream" }; + + return new File( + [blob], + `${idx}-${await sha256(JSON.stringify(params))}.${ext}`, + { + type: mime, + } + ); + }) + .then((file) => uploadFile(file, ctx.conv)) + .then((file) => files.push(file)); + }) + ); + + toolOutputs.push({ + [tool.name + "-" + idx.toString()]: + `Only and always answer: 'I used the tool ${tool.displayName}, here is the result.' Don't add anything else.`, + }); + } else { + for (const output of arrayedOutput) { + toolOutputs.push({ + [tool.name + "-" + idx.toString()]: output, + }); + } + } + }) + ); + + for (const file of files) { + yield { + type: MessageUpdateType.File, + name: file.name, + sha: file.value, + mime: file.mime, + }; + } + + return { outputs: toolOutputs, display: tool.showOutput }; + }; +} + +export const toolFromConfigs = configTools.parse(JSON5.parse(env.TOOLS)) satisfies ConfigTool[]; diff --git a/src/lib/server/tools/outputs.ts b/src/lib/server/tools/outputs.ts new file mode 100644 index 0000000000000000000000000000000000000000..d8484c6abd22d6f29af87672747fb47f0ca06595 --- /dev/null +++ b/src/lib/server/tools/outputs.ts @@ -0,0 +1,52 @@ +import type { ToolIOType, ToolOutputComponents } from "$lib/types/Tool"; + +export const ToolOutputPaths: Record< + ToolOutputComponents, + { + type: ToolIOType; + path: string; + } +> = { + textbox: { + type: "str", + path: "$", + }, + markdown: { + type: "str", + path: "$", + }, + number: { + type: "float", + path: "$", + }, + image: { + type: "file", + path: "$.url", + }, + gallery: { + type: "file", + path: "$[*].image.url", + }, + audio: { + type: "file", + path: "$.url", + }, + video: { + type: "file", + path: "$.video.url", + }, + file: { + type: "file", + path: "$.url", + }, + json: { + type: "str", + path: "$", + }, +}; + +export const isValidOutputComponent = ( + outputComponent: string +): outputComponent is keyof typeof ToolOutputPaths => { + return Object.keys(ToolOutputPaths).includes(outputComponent); +}; diff --git a/src/lib/server/tools/utils.ts b/src/lib/server/tools/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc0bd281847874c141584dc0993455cde70e2169 --- /dev/null +++ b/src/lib/server/tools/utils.ts @@ -0,0 +1,115 @@ +import { env } from "$env/dynamic/private"; +import { Client } from "@gradio/client"; +import { SignJWT } from "jose"; +import JSON5 from "json5"; +import { + MessageToolUpdateType, + MessageUpdateType, + type MessageToolUpdate, +} from "$lib/types/MessageUpdate"; +import { logger } from "$lib/server/logger"; +export async function* callSpace( + name: string, + func: string, + parameters: TInput, + ipToken: string | undefined, + uuid: string +): AsyncGenerator { + class CustomClient extends Client { + fetch(input: RequestInfo | URL, init?: RequestInit): Promise { + init = init || {}; + init.headers = { + ...(init.headers || {}), + ...(ipToken ? { "X-IP-Token": ipToken } : {}), + }; + return super.fetch(input, init); + } + } + const client = await CustomClient.connect(name, { + hf_token: ipToken // dont pass the hf token if we have an ip token + ? undefined + : ((env.HF_TOKEN ?? env.HF_ACCESS_TOKEN) as unknown as `hf_${string}`), + events: ["status", "data"], + }); + + const job = client.submit(func, parameters); + + let data; + for await (const output of job) { + if (output.type === "data") { + data = output.data as TOutput; + } + if (output.type === "status") { + if (output.stage === "error") { + logger.error(output.message); + throw new Error(output.message); + } + if (output.eta) { + yield { + type: MessageUpdateType.Tool, + subtype: MessageToolUpdateType.ETA, + eta: output.eta, + uuid, + }; + } + } + } + + if (!data) { + throw new Error("No data found in tool call"); + } + + return data; +} + +export async function getIpToken(ip: string, username?: string) { + const ipTokenSecret = env.IP_TOKEN_SECRET; + if (!ipTokenSecret) { + return; + } + return await new SignJWT({ ip, user: username }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("1m") + .sign(new TextEncoder().encode(ipTokenSecret)); +} + +export { toolHasName } from "$lib/utils/tools"; + +export async function extractJson(text: string): Promise { + const calls: string[] = []; + + let codeBlocks = Array.from(text.matchAll(/```json\n(.*?)```/gs)) + .map(([, block]) => block) + // remove trailing comma + .map((block) => block.trim().replace(/,$/, "")); + + // if there is no code block, try to find the first json object + // by trimming the string and trying to parse with JSON5 + if (codeBlocks.length === 0) { + const start = [text.indexOf("["), text.indexOf("{")] + .filter((i) => i !== -1) + .reduce((a, b) => Math.max(a, b), -Infinity); + const end = [text.lastIndexOf("]"), text.lastIndexOf("}")] + .filter((i) => i !== -1) + .reduce((a, b) => Math.min(a, b), Infinity); + + if (start === -Infinity || end === Infinity) { + return [""]; + } + + const json = text.substring(start, end + 1); + codeBlocks = [json]; + } + + // grab only the capture group from the regex match + for (const block of codeBlocks) { + // make it an array if it's not already + let call = JSON5.parse(block); + if (!Array.isArray(call)) { + call = [call]; + } + calls.push(call); + } + return calls.flat(); +} diff --git a/src/lib/server/tools/web/search.ts b/src/lib/server/tools/web/search.ts new file mode 100644 index 0000000000000000000000000000000000000000..b181d661bfd46ec814d69b1215c068853c4693d6 --- /dev/null +++ b/src/lib/server/tools/web/search.ts @@ -0,0 +1,46 @@ +import type { ConfigTool } from "$lib/types/Tool"; +import { ObjectId } from "mongodb"; +import { runWebSearch } from "../../websearch/runWebSearch"; + +const websearch: ConfigTool = { + _id: new ObjectId("00000000000000000000000A"), + type: "config", + description: "Search the web for answers to the user's query", + color: "blue", + icon: "wikis", + displayName: "Web Search", + name: "websearch", + endpoint: null, + inputs: [ + { + name: "query", + type: "str", + description: + "A search query which will be used to fetch the most relevant snippets regarding the user's query", + paramType: "required", + }, + ], + outputComponent: null, + outputComponentIdx: null, + showOutput: false, + async *call({ query }, { conv, assistant, messages }) { + const webSearchToolResults = yield* runWebSearch(conv, messages, assistant?.rag, String(query)); + + const webSearchContext = webSearchToolResults?.contextSources + .map(({ context }, idx) => `Source [${idx + 1}]\n${context.trim()}`) + .join("\n\n----------\n\n"); + + return { + outputs: [ + { + websearch: + webSearchContext + + "\n\nWhen answering the question, you must reference the sources you used inline by wrapping the index in brackets like this: [1]. If multiple sources are used, you must reference each one of them without commas like this: [1][2][3].", + }, + ], + display: false, + }; + }, +}; + +export default websearch; diff --git a/src/lib/server/tools/web/url.ts b/src/lib/server/tools/web/url.ts new file mode 100644 index 0000000000000000000000000000000000000000..e77ff185da72c2e488772486036c09f11b512e13 --- /dev/null +++ b/src/lib/server/tools/web/url.ts @@ -0,0 +1,39 @@ +import { stringifyMarkdownElementTree } from "$lib/server/websearch/markdown/utils/stringify"; +import { scrapeUrl } from "$lib/server/websearch/scrape/scrape"; +import type { ConfigTool } from "$lib/types/Tool"; +import { ObjectId } from "mongodb"; + +const fetchUrl: ConfigTool = { + _id: new ObjectId("00000000000000000000000B"), + type: "config", + description: "Fetch the contents of a URL", + color: "blue", + icon: "cloud", + displayName: "Fetch URL", + name: "fetchUrl", + endpoint: null, + inputs: [ + { + name: "url", + type: "str", + description: "The URL of the webpage to fetch", + paramType: "required", + }, + ], + outputComponent: null, + outputComponentIdx: null, + showOutput: false, + async *call({ url }) { + const blocks = String(url).split("\n"); + const urlStr = blocks[blocks.length - 1]; + + const { title, markdownTree } = await scrapeUrl(urlStr, Infinity); + + return { + outputs: [{ title, text: stringifyMarkdownElementTree(markdownTree) }], + display: false, + }; + }, +}; + +export default fetchUrl; diff --git a/src/lib/server/usageLimits.ts b/src/lib/server/usageLimits.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca979ea59f02567bb5eb80ce9b912d0247585dd2 --- /dev/null +++ b/src/lib/server/usageLimits.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; +import { env } from "$env/dynamic/private"; +import JSON5 from "json5"; + +// RATE_LIMIT is the legacy way to define messages per minute limit +export const usageLimitsSchema = z + .object({ + conversations: z.coerce.number().optional(), // how many conversations + messages: z.coerce.number().optional(), // how many messages in a conversation + assistants: z.coerce.number().optional(), // how many assistants + messageLength: z.coerce.number().optional(), // how long can a message be before we cut it off + messagesPerMinute: z + .preprocess((val) => { + if (val === undefined) { + return env.RATE_LIMIT; + } + return val; + }, z.coerce.number().optional()) + .optional(), // how many messages per minute + tools: z.coerce.number().optional(), // how many tools + }) + .optional(); + +export const usageLimits = usageLimitsSchema.parse(JSON5.parse(env.USAGE_LIMITS)); diff --git a/src/lib/server/websearch/embed/combine.ts b/src/lib/server/websearch/embed/combine.ts new file mode 100644 index 0000000000000000000000000000000000000000..29b4113c2d89658e4725f94ea00f4b08153a1e0e --- /dev/null +++ b/src/lib/server/websearch/embed/combine.ts @@ -0,0 +1,37 @@ +import type { EmbeddingBackendModel } from "$lib/server/embeddingModels"; +import { getSentenceSimilarity } from "$lib/server/sentenceSimilarity"; + +/** + * Combines sentences together to reach the maximum character limit of the embedding model + * Improves performance considerably when using CPU embedding + */ +export async function getCombinedSentenceSimilarity( + embeddingModel: EmbeddingBackendModel, + query: string, + sentences: string[] +): ReturnType { + const combinedSentences = sentences.reduce<{ text: string; indices: number[] }[]>( + (acc, sentence, idx) => { + const lastSentence = acc[acc.length - 1]; + if (!lastSentence) return [{ text: sentence, indices: [idx] }]; + if (lastSentence.text.length + sentence.length < embeddingModel.chunkCharLength) { + lastSentence.text += ` ${sentence}`; + lastSentence.indices.push(idx); + return acc; + } + return [...acc, { text: sentence, indices: [idx] }]; + }, + [] + ); + + const embeddings = await getSentenceSimilarity( + embeddingModel, + query, + combinedSentences.map(({ text }) => text) + ); + + return embeddings.flatMap((embedding, idx) => { + const { indices } = combinedSentences[idx]; + return indices.map((i) => ({ ...embedding, idx: i })); + }); +} diff --git a/src/lib/server/websearch/embed/embed.ts b/src/lib/server/websearch/embed/embed.ts new file mode 100644 index 0000000000000000000000000000000000000000..aba7dee13bc16393450c4baf4aee75f64b0b6f85 --- /dev/null +++ b/src/lib/server/websearch/embed/embed.ts @@ -0,0 +1,85 @@ +import { MetricsServer } from "$lib/server/metrics"; +import type { WebSearchScrapedSource, WebSearchUsedSource } from "$lib/types/WebSearch"; +import type { EmbeddingBackendModel } from "../../embeddingModels"; +import { getSentenceSimilarity, innerProduct } from "../../sentenceSimilarity"; +import { MarkdownElementType, type MarkdownElement } from "../markdown/types"; +import { stringifyMarkdownElement } from "../markdown/utils/stringify"; +import { getCombinedSentenceSimilarity } from "./combine"; +import { flattenTree } from "./tree"; + +const MIN_CHARS = 3_000; +const SOFT_MAX_CHARS = 8_000; + +export async function findContextSources( + sources: WebSearchScrapedSource[], + prompt: string, + embeddingModel: EmbeddingBackendModel +) { + const startTime = Date.now(); + + const sourcesMarkdownElems = sources.map((source) => flattenTree(source.page.markdownTree)); + const markdownElems = sourcesMarkdownElems.flat(); + + // When using CPU embedding (transformersjs), join sentences together to the max character limit + // to reduce inference time + const embeddingFunc = + embeddingModel.endpoints[0].type === "transformersjs" + ? getCombinedSentenceSimilarity + : getSentenceSimilarity; + + const embeddings = await embeddingFunc( + embeddingModel, + prompt, + markdownElems + .map(stringifyMarkdownElement) + // Safety in case the stringified markdown elements are too long + // but chunking should have happened earlier + .map((elem) => elem.slice(0, embeddingModel.chunkCharLength)) + ); + + const topEmbeddings = embeddings + .sort((a, b) => a.distance - b.distance) + .filter((embedding) => markdownElems[embedding.idx].type !== MarkdownElementType.Header); + + let totalChars = 0; + const selectedMarkdownElems = new Set(); + const selectedEmbeddings: number[][] = []; + for (const embedding of topEmbeddings) { + const elem = markdownElems[embedding.idx]; + + // Ignore elements that are too similar to already selected elements + const tooSimilar = selectedEmbeddings.some( + (selectedEmbedding) => innerProduct(selectedEmbedding, embedding.embedding) < 0.01 + ); + if (tooSimilar) continue; + + // Add element + if (!selectedMarkdownElems.has(elem)) { + selectedMarkdownElems.add(elem); + selectedEmbeddings.push(embedding.embedding); + totalChars += elem.content.length; + } + + // Add element's parent (header) + if (elem.parent && !selectedMarkdownElems.has(elem.parent)) { + selectedMarkdownElems.add(elem.parent); + totalChars += elem.parent.content.length; + } + + if (totalChars > SOFT_MAX_CHARS) break; + if (totalChars > MIN_CHARS && embedding.distance > 0.25) break; + } + + const contextSources = sourcesMarkdownElems + .map((elems, idx) => { + const sourceSelectedElems = elems.filter((elem) => selectedMarkdownElems.has(elem)); + const context = sourceSelectedElems.map(stringifyMarkdownElement).join("\n"); + const source = sources[idx]; + return { ...source, context }; + }) + .filter((contextSource) => contextSource.context.length > 0); + + MetricsServer.getMetrics().webSearch.embeddingDuration.observe(Date.now() - startTime); + + return contextSources; +} diff --git a/src/lib/server/websearch/embed/tree.ts b/src/lib/server/websearch/embed/tree.ts new file mode 100644 index 0000000000000000000000000000000000000000..f40e4eb6f08ea2efd3f986efe03491d9f0606661 --- /dev/null +++ b/src/lib/server/websearch/embed/tree.ts @@ -0,0 +1,6 @@ +import type { MarkdownElement } from "../markdown/types"; + +export function flattenTree(elem: MarkdownElement): MarkdownElement[] { + if ("children" in elem) return [elem, ...elem.children.flatMap(flattenTree)]; + return [elem]; +} diff --git a/src/lib/server/websearch/markdown/fromHtml.ts b/src/lib/server/websearch/markdown/fromHtml.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee58d398c333143882916fbcc353569f6b1ce400 --- /dev/null +++ b/src/lib/server/websearch/markdown/fromHtml.ts @@ -0,0 +1,98 @@ +import { collapseString, sanitizeString } from "./utils/nlp"; +import { stringifyHTMLElements, stringifyHTMLElementsUnformatted } from "./utils/stringify"; +import { MarkdownElementType, tagNameMap, type HeaderElement, type MarkdownElement } from "./types"; +import type { SerializedHTMLElement } from "../scrape/types"; + +interface ConversionState { + defaultType: + | MarkdownElementType.Paragraph + | MarkdownElementType.BlockQuote + | MarkdownElementType.UnorderedListItem + | MarkdownElementType.OrderedListItem; + listDepth: number; + blockQuoteDepth: number; +} +export function htmlElementToMarkdownElements( + parent: HeaderElement, + elem: SerializedHTMLElement | string, + prevState: ConversionState = { + defaultType: MarkdownElementType.Paragraph, + listDepth: 0, + blockQuoteDepth: 0, + } +): MarkdownElement | MarkdownElement[] { + // Found text so create an element based on the previous state + if (typeof elem === "string") { + if (elem.trim().length === 0) return []; + if ( + prevState.defaultType === MarkdownElementType.UnorderedListItem || + prevState.defaultType === MarkdownElementType.OrderedListItem + ) { + return { + parent, + type: prevState.defaultType, + content: elem, + depth: prevState.listDepth, + }; + } + if (prevState.defaultType === MarkdownElementType.BlockQuote) { + return { + parent, + type: prevState.defaultType, + content: elem, + depth: prevState.blockQuoteDepth, + }; + } + return { parent, type: prevState.defaultType, content: elem }; + } + + const type = tagNameMap[elem.tagName] ?? MarkdownElementType.Paragraph; + + // Update the state based on the current element + const state: ConversionState = { ...prevState }; + if (type === MarkdownElementType.UnorderedList || type === MarkdownElementType.OrderedList) { + state.listDepth += 1; + state.defaultType = + type === MarkdownElementType.UnorderedList + ? MarkdownElementType.UnorderedListItem + : MarkdownElementType.OrderedListItem; + } + if (type === MarkdownElementType.BlockQuote) { + state.defaultType = MarkdownElementType.BlockQuote; + state.blockQuoteDepth += 1; + } + + // Headers + if (type === MarkdownElementType.Header) { + return { + parent, + type, + level: Number(elem.tagName[1]), + content: collapseString(stringifyHTMLElements(elem.content)), + children: [], + }; + } + + // Code blocks + if (type === MarkdownElementType.CodeBlock) { + return { + parent, + type, + content: sanitizeString(stringifyHTMLElementsUnformatted(elem.content)), + }; + } + + // Typical case, we want to flatten the DOM and only create elements when we see text + return elem.content.flatMap((el) => htmlElementToMarkdownElements(parent, el, state)); +} + +export function mergeAdjacentElements(elements: MarkdownElement[]): MarkdownElement[] { + return elements.reduce((acc, elem) => { + const last = acc[acc.length - 1]; + if (last && last.type === MarkdownElementType.Paragraph && last.type === elem.type) { + last.content += elem.content; + return acc; + } + return [...acc, elem]; + }, []); +} diff --git a/src/lib/server/websearch/markdown/tree.ts b/src/lib/server/websearch/markdown/tree.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd4b652aed6c8ba06798bcf12107ffbc651da708 --- /dev/null +++ b/src/lib/server/websearch/markdown/tree.ts @@ -0,0 +1,63 @@ +import type { SerializedHTMLElement } from "../scrape/types"; +import { htmlElementToMarkdownElements, mergeAdjacentElements } from "./fromHtml"; +import type { HeaderElement, MarkdownElement } from "./types"; +import { MarkdownElementType } from "./types"; +import { chunkElements } from "./utils/chunk"; + +/** + * Converts HTML elements to Markdown elements and creates a tree based on header tags + * For example: h1 [h2 [p p blockquote] h2 [h3 [...] ] ] + **/ +export function htmlToMarkdownTree( + title: string, + htmlElements: SerializedHTMLElement[], + maxCharsPerElem: number +): HeaderElement { + let parent: HeaderElement = { + type: MarkdownElementType.Header, + level: 1, + parent: null, + content: title, + children: [], + }; + + const markdownElements = chunkElements( + mergeAdjacentElements( + htmlElements.flatMap((elem) => htmlElementToMarkdownElements(parent, elem)) + ), + maxCharsPerElem + ); + + for (const elem of markdownElements) { + if (elem.type !== MarkdownElementType.Header) { + elem.parent = parent; + parent.children.push(elem); + continue; + } + + // add 1 to current level to offset for the title being level 1 + elem.level += 1; + + // Pop up header levels until reaching the same level as the current header + // or until we reach the root + inner: while (parent !== null && parent.parent !== null) { + if (parent.level < elem.level) break inner; + parent = parent.parent; + } + parent.children.push(elem); + parent = elem; + } + + // Pop up to the root + while (parent.parent !== null) { + parent = parent.parent; + } + return parent; +} + +export function removeParents(elem: T): T { + if ("children" in elem) { + return { ...elem, parent: null, children: elem.children.map((child) => removeParents(child)) }; + } + return { ...elem, parent: null }; +} diff --git a/src/lib/server/websearch/markdown/types.ts b/src/lib/server/websearch/markdown/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..e4c99f0369c95f81cb8a83a7980602f7945a9c6b --- /dev/null +++ b/src/lib/server/websearch/markdown/types.ts @@ -0,0 +1,55 @@ +/* eslint-disable-next-line no-shadow */ +export enum MarkdownElementType { + Header = "HEADER", + Paragraph = "PARAGRAPH", + BlockQuote = "BLOCKQUOTE", + CodeBlock = "CODE_BLOCK", + + UnorderedList = "UNORDERED_LIST", + OrderedList = "ORDERED_LIST", + UnorderedListItem = "UNORDERED_LIST_ITEM", + OrderedListItem = "ORDERED_LIST_ITEM", +} + +interface BaseMarkdownElement { + type: T; + content: string; + parent: HeaderElement | null; +} + +export interface HeaderElement extends BaseMarkdownElement { + level: number; + children: MarkdownElement[]; +} +type ListItem = MarkdownElementType.UnorderedListItem | MarkdownElementType.OrderedListItem; +interface ListItemElement extends BaseMarkdownElement { + depth: number; +} +interface BlockQuoteElement extends BaseMarkdownElement { + depth: number; +} +interface ParagraphElement extends BaseMarkdownElement {} +interface CodeBlockElement extends BaseMarkdownElement {} + +export type MarkdownElement = + | HeaderElement + | ParagraphElement + | BlockQuoteElement + | CodeBlockElement + | ListItemElement; + +export const tagNameMap: Record = { + h1: MarkdownElementType.Header, + h2: MarkdownElementType.Header, + h3: MarkdownElementType.Header, + h4: MarkdownElementType.Header, + h5: MarkdownElementType.Header, + h6: MarkdownElementType.Header, + div: MarkdownElementType.Paragraph, + p: MarkdownElementType.Paragraph, + blockquote: MarkdownElementType.BlockQuote, + pre: MarkdownElementType.CodeBlock, + ul: MarkdownElementType.UnorderedList, + ol: MarkdownElementType.OrderedList, + li: MarkdownElementType.UnorderedListItem, +}; diff --git a/src/lib/server/websearch/markdown/utils/chunk.ts b/src/lib/server/websearch/markdown/utils/chunk.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed035fbb01664fac5f979303c816633c5bb76953 --- /dev/null +++ b/src/lib/server/websearch/markdown/utils/chunk.ts @@ -0,0 +1,60 @@ +import { sentences as splitBySentences } from "sbd"; +import { MarkdownElementType, type MarkdownElement } from "../types"; + +export function chunkElements(elements: MarkdownElement[], maxLength: number): MarkdownElement[] { + return elements.flatMap((elem) => { + // Can't split headers because it would break the tree, and this situation should be rare + // so we just cut off the end + if (elem.type === MarkdownElementType.Header) { + return { ...elem, content: elem.content.slice(0, maxLength) }; + } + const contentChunks = enforceMaxLength(elem.content, maxLength); + return contentChunks.map((content) => ({ ...elem, content })); + }); +} + +const delimitersByPriority = ["?", "!", ".", ";", ":", ",", "|", " - ", " ", "-"]; +function enforceMaxLength(text: string, maxLength: number): string[] { + if (text.length <= maxLength) return [text].filter(Boolean); + return splitBySentences(text) + .flatMap((sentence) => { + if (sentence.length <= maxLength) return sentence; + + // Discover all necessary split points to fit the sentence within the max length + const indices: [number, number][] = []; + while ((indices.at(-1)?.[1] ?? 0) < sentence.length) { + const prevIndex = indices.at(-1)?.[1] ?? 0; + + // Remaining text fits within maxLength + if (prevIndex + maxLength >= sentence.length) { + indices.push([prevIndex, sentence.length]); + continue; + } + + const bestDelimiter = delimitersByPriority.find( + (delimiter) => sentence.lastIndexOf(delimiter, prevIndex + maxLength) !== -1 + ); + // Fallback in the unusual case that no delimiter is found + if (!bestDelimiter) { + indices.push([prevIndex, prevIndex + maxLength]); + continue; + } + + const closestDelimiter = sentence.lastIndexOf(bestDelimiter, prevIndex + maxLength); + indices.push([prevIndex, Math.max(prevIndex + 1, closestDelimiter)]); + } + + return indices.map((sliceIndices) => sentence.slice(...sliceIndices)); + }) + .reduce( + (chunks, sentence) => { + const lastChunk = chunks[chunks.length - 1]; + if (lastChunk.length + sentence.length <= maxLength) { + return [...chunks.slice(0, -1), lastChunk + sentence]; + } + return [...chunks, sentence]; + }, + [""] + ) + .filter(Boolean); +} diff --git a/src/lib/server/websearch/markdown/utils/nlp.ts b/src/lib/server/websearch/markdown/utils/nlp.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a49a77122d5fddeb67329a2c6cdbb8a628fb7e7 --- /dev/null +++ b/src/lib/server/websearch/markdown/utils/nlp.ts @@ -0,0 +1,11 @@ +/** Remove excess whitespace and newlines */ +export const sanitizeString = (str: string) => + str + .split("\n") + .map((s) => s.trim()) + .filter(Boolean) + .join("\n") + .replaceAll(/ +/g, " "); + +/** Collapses a string into a single line */ +export const collapseString = (str: string) => sanitizeString(str.replaceAll(/\n/g, " ")); diff --git a/src/lib/server/websearch/markdown/utils/stringify.ts b/src/lib/server/websearch/markdown/utils/stringify.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e6941f7182a1dd9f04efbf042f7abb1e73c98b7 --- /dev/null +++ b/src/lib/server/websearch/markdown/utils/stringify.ts @@ -0,0 +1,82 @@ +import type { SerializedHTMLElement } from "../../scrape/types"; +import { MarkdownElementType, type MarkdownElement } from "../types"; + +// --- Markdown Elements --- + +/** Converts markdown element to a string with formatting */ +export function stringifyMarkdownElement(elem: MarkdownElement): string { + const content = elem.content.trim(); + if (elem.type === MarkdownElementType.Header) return `${"#".repeat(elem.level)} ${content}\n\n`; + if (elem.type === MarkdownElementType.BlockQuote) { + return `${"> ".repeat(elem.depth)}${content}\n\n`; + } + if (elem.type === MarkdownElementType.CodeBlock) return `\`\`\`\n${content}\n\`\`\`\n\n`; + + if (elem.type === MarkdownElementType.UnorderedListItem) return `- ${content}\n`; + if (elem.type === MarkdownElementType.OrderedListItem) { + const siblings = elem.parent?.children ?? [elem]; + const currentIndex = siblings.indexOf(elem); + const lastAdjacentIndex = siblings + .slice(currentIndex + 1) + .findLastIndex((child) => child.type === MarkdownElementType.OrderedListItem); + const order = currentIndex - lastAdjacentIndex + 1; + return `${order}. ${content}\n`; + } + + return `${content}\n\n`; +} + +/** Converts a tree of markdown elements to a string with formatting */ +export function stringifyMarkdownElementTree(elem: MarkdownElement): string { + const stringified = stringifyMarkdownElement(elem); + if (!("children" in elem)) return stringified; + return stringified + elem.children.map(stringifyMarkdownElementTree).join(""); +} + +// ----- HTML Elements ----- + +/** Ignores all non-inline tag types and grabs their text. Converts inline tags to markdown */ +export function stringifyHTMLElements(elems: (SerializedHTMLElement | string)[]): string { + return elems.map(stringifyHTMLElement).join("").trim(); +} + +/** Ignores all non-inline tag types and grabs their text. Converts inline tags to markdown */ +export function stringifyHTMLElement(elem: SerializedHTMLElement | string): string { + if (typeof elem === "string") return elem; + if (elem.tagName === "br") return "\n"; + + const content = elem.content.map(stringifyHTMLElement).join(""); + if (content.length === 0) return content; + + if (elem.tagName === "strong" || elem.tagName === "b") return `**${content}**`; + if (elem.tagName === "em" || elem.tagName === "i") return `*${content}*`; + if (elem.tagName === "s" || elem.tagName === "strike") return `~~${content}~~`; + + if (elem.tagName === "code" || elem.tagName === "var" || elem.tagName === "tt") { + return `\`${content}\``; + } + + if (elem.tagName === "sup") return `${content}`; + if (elem.tagName === "sub") return `${content}`; + + if (elem.tagName === "a" && content.trim().length > 0) { + const href = elem.attributes.href; + if (!href) return elem.content.map(stringifyHTMLElement).join(""); + return `[${elem.content.map(stringifyHTMLElement).join("")}](${href})`; + } + + return elem.content.map(stringifyHTMLElement).join(""); +} + +/** Grabs all text content directly, ignoring HTML tags */ +export function stringifyHTMLElementsUnformatted( + elems: (SerializedHTMLElement | string)[] +): string { + return elems.map(stringifyHTMLElementUnformatted).join(""); +} + +/** Grabs all text content directly, ignoring HTML tags */ +function stringifyHTMLElementUnformatted(elem: SerializedHTMLElement | string): string { + if (typeof elem === "string") return elem; + return elem.content.map(stringifyHTMLElementUnformatted).join(""); +} diff --git a/src/lib/server/websearch/runWebSearch.ts b/src/lib/server/websearch/runWebSearch.ts new file mode 100644 index 0000000000000000000000000000000000000000..39f203b1a38ea9587d85d614849d6b130cd8caff --- /dev/null +++ b/src/lib/server/websearch/runWebSearch.ts @@ -0,0 +1,104 @@ +import { defaultEmbeddingModel, embeddingModels } from "$lib/server/embeddingModels"; + +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import type { WebSearch, WebSearchScrapedSource } from "$lib/types/WebSearch"; +import type { Assistant } from "$lib/types/Assistant"; +import type { MessageWebSearchUpdate } from "$lib/types/MessageUpdate"; + +import { search } from "./search/search"; +import { scrape } from "./scrape/scrape"; +import { findContextSources } from "./embed/embed"; +import { removeParents } from "./markdown/tree"; +import { + makeErrorUpdate, + makeFinalAnswerUpdate, + makeGeneralUpdate, + makeSourcesUpdate, +} from "./update"; +import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators"; +import { MetricsServer } from "../metrics"; +import { logger } from "$lib/server/logger"; + +const MAX_N_PAGES_TO_SCRAPE = 8 as const; +const MAX_N_PAGES_TO_EMBED = 5 as const; + +export async function* runWebSearch( + conv: Conversation, + messages: Message[], + ragSettings?: Assistant["rag"], + query?: string +): AsyncGenerator { + const prompt = messages[messages.length - 1].content; + const createdAt = new Date(); + const updatedAt = new Date(); + + MetricsServer.getMetrics().webSearch.requestCount.inc(); + + try { + const embeddingModel = + embeddingModels.find((m) => m.id === conv.embeddingModel) ?? defaultEmbeddingModel; + if (!embeddingModel) { + throw Error(`Embedding model ${conv.embeddingModel} not available anymore`); + } + + // Search the web + const { searchQuery, pages } = yield* search(messages, ragSettings, query); + if (pages.length === 0) throw Error("No results found for this search query"); + + // Scrape pages + yield makeGeneralUpdate({ message: "Browsing search results" }); + + const allScrapedPages = yield* mergeAsyncGenerators( + pages.slice(0, MAX_N_PAGES_TO_SCRAPE).map(scrape(embeddingModel.chunkCharLength)) + ); + const scrapedPages = allScrapedPages + .filter((p): p is WebSearchScrapedSource => Boolean(p)) + .filter((p) => p.page.markdownTree.children.length > 0) + .slice(0, MAX_N_PAGES_TO_EMBED); + + if (!scrapedPages.length) { + throw Error(`No text found in the first ${MAX_N_PAGES_TO_SCRAPE} results`); + } + + // Chunk the text of each of the elements and find the most similar chunks to the prompt + yield makeGeneralUpdate({ message: "Extracting relevant information" }); + const contextSources = await findContextSources(scrapedPages, prompt, embeddingModel).then( + (ctxSources) => + ctxSources.map((source) => ({ + ...source, + page: { ...source.page, markdownTree: removeParents(source.page.markdownTree) }, + })) + ); + yield makeSourcesUpdate(contextSources); + + const webSearch: WebSearch = { + prompt, + searchQuery, + results: scrapedPages.map(({ page, ...source }) => ({ + ...source, + page: { ...page, markdownTree: removeParents(page.markdownTree) }, + })), + contextSources, + createdAt, + updatedAt, + }; + yield makeFinalAnswerUpdate(); + return webSearch; + } catch (searchError) { + const message = searchError instanceof Error ? searchError.message : String(searchError); + logger.error(message); + yield makeErrorUpdate({ message: "An error occurred", args: [message] }); + + const webSearch: WebSearch = { + prompt, + searchQuery: "", + results: [], + contextSources: [], + createdAt, + updatedAt, + }; + yield makeFinalAnswerUpdate(); + return webSearch; + } +} diff --git a/src/lib/server/websearch/scrape/parser.ts b/src/lib/server/websearch/scrape/parser.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5502f820a711bf8022ac4f6f41baa10da9856d8 --- /dev/null +++ b/src/lib/server/websearch/scrape/parser.ts @@ -0,0 +1,555 @@ +import type { SerializedHTMLElement } from "./types"; + +interface DBSCANOptions { + dataset: T[]; + epsilon?: number; + epsilonCompare?: (distance: number, epsilon: number) => boolean; + minimumPoints?: number; + distanceFunction: (a: T, b: T) => number; +} + +export function spatialParser() { + /** + * Implementation for dbscan, inlined and migrated to typescript from https://github.com/cdxOo/dbscan (MIT License) + */ + const DBSCAN = ({ + dataset, + epsilon = 1, + epsilonCompare = (dist, e) => dist < e, + minimumPoints = 2, + distanceFunction, + }: DBSCANOptions) => { + const visitedIndices: Record = {}; + const isVisited = (i: number) => visitedIndices[i]; + const markVisited = (i: number) => { + visitedIndices[i] = true; + }; + + const clusteredIndices: Record = {}; + const isClustered = (i: number) => clusteredIndices[i]; + const markClustered = (i: number) => { + clusteredIndices[i] = true; + }; + + const uniqueMerge = (targetArray: U[], sourceArray: U[]) => { + for (let i = 0; i < sourceArray.length; i += 1) { + const item = sourceArray[i]; + if (targetArray.indexOf(item) < 0) { + targetArray.push(item); + } + } + }; + + const findNeighbors = (index: number) => { + const neighbors = []; + for (let other = 0; other < dataset.length; other += 1) { + const distance = distanceFunction(dataset[index], dataset[other]); + if (epsilonCompare(distance, epsilon)) { + neighbors.push(other); + } + } + return neighbors; + }; + + const noise: number[] = []; + const addNoise = (i: number) => noise.push(i); + + const clusters: number[][] = []; + const createCluster = () => clusters.push([]) - 1; + const addIndexToCluster = (c: number, i: number) => { + clusters[c].push(i); + markClustered(i); + }; + + const expandCluster = (c: number, neighbors: number[]) => { + for (let i = 0; i < neighbors.length; i += 1) { + const neighborIndex = neighbors[i]; + if (!isVisited(neighborIndex)) { + markVisited(neighborIndex); + + const secondaryNeighbors = findNeighbors(neighborIndex); + if (secondaryNeighbors.length >= minimumPoints) { + uniqueMerge(neighbors, secondaryNeighbors); + } + } + + if (!isClustered(neighborIndex)) { + addIndexToCluster(c, neighborIndex); + } + } + }; + + dataset.forEach((_, index) => { + if (!isVisited(index)) { + markVisited(index); + + const neighbors = findNeighbors(index); + if (neighbors.length < minimumPoints) { + addNoise(index); + } else { + const clusterIndex = createCluster(); + addIndexToCluster(clusterIndex, index); + expandCluster(clusterIndex, neighbors); + } + } + }); + + return { clusters, noise }; + }; + + // ----------- + // Scraping implementation + + const IgnoredTagsList = [ + "footer", + "nav", + "aside", + "script", + "style", + "noscript", + "form", + "button", + ]; + const InlineTags = [ + "a", + "abbrv", + "span", + "address", + "time", + "acronym", + "strong", + "b", + "br", + "sub", + "sup", + "tt", + "var", + "em", + "i", + ]; + + type ReadableNode = HTMLElement; + type NodeWithRect = { + node: ReadableNode; + rect: DOMRect; + }; + + const isOnlyChild = (node: Node) => { + if (!node.parentElement) return true; + if (node.parentElement.nodeName === "body") return false; + if (node.parentElement.childNodes.length === 1) return true; + return false; + }; + + const hasValidInlineParent = (node: Node) => { + return node.parentElement && !node.parentElement.matches("div, section, article, main, body "); + }; + + const hasValidParent = (node: Node) => { + return node.parentElement && !node.parentElement.isSameNode(document.body); + }; + + const possibleCodeParents = Array.from(document.querySelectorAll("pre, p")); + const possibleTableParents = Array.from(document.querySelectorAll("table")); + const possibleListParents = Array.from(document.querySelectorAll("ul, ol")); + /** + * We want to find the highest parent of text node in the cluster. + * For example in this case:

Text here

+ * the P tag is highest parent. + */ + const findHighestDirectParentOfReadableNode = (node: Node): HTMLElement => { + // go up the tree until the parent is no longer an only child + let parent = node.parentElement; + // if the parent is an inline tag, then go up one more level + while ( + parent && + hasValidInlineParent(parent) && + InlineTags.includes(parent?.tagName.toLowerCase()) + ) { + parent = parent.parentElement; + } + + while (parent && isOnlyChild(parent)) { + if (!hasValidParent(parent)) break; + parent = parent.parentElement; + } + + if (!parent) { + throw new Error( + "disconnected node found, this should not really be possible when traversing through the dom" + ); + } + + // if the parent is a span, code or div tag check if there is a pre tag or p tag above it + if (["span", "code", "div"].includes(parent.nodeName.toLowerCase())) { + const hasParent = possibleCodeParents.find((tag) => tag.contains(parent)) as HTMLElement; + if (hasParent) { + parent = hasParent; + } + } + + // if the parent is a li tag check if there is a ul or ol tag above it + if (parent.nodeName.toLowerCase() === "li") { + const hasParent = possibleListParents.find((tag) => tag.contains(parent)) as HTMLElement; + if (hasParent) { + parent = hasParent; + } + } + + // if the parent is a td, th, tr tag check if there is a table tag above it + if (["td", "th", "tr"].includes(parent.nodeName.toLowerCase())) { + const hasParent = possibleTableParents.find((tag) => tag.contains(parent)) as HTMLElement; + if (hasParent) { + parent = hasParent; + } + } + + return parent; + }; + const barredNodes = Array.from(document.querySelectorAll(IgnoredTagsList.join(","))); + + const doesNodePassHeuristics = (node: Node) => { + if ((node.textContent ?? "").trim().length < 10) { + return false; + } + + const parentNode = findHighestDirectParentOfReadableNode(node); + + if (parentNode && parentNode instanceof Element) { + if ( + !parentNode.checkVisibility({ + checkOpacity: true, + checkVisibilityCSS: true, + }) + ) + return false; + + const rect = parentNode.getBoundingClientRect(); + // elements that are readable usually don't have really small height or width + if (rect.width < 4 || rect.height < 4) { + return false; + } + } + + if (parentNode && parentNode instanceof Element) { + if (barredNodes.some((barredNode) => barredNode.contains(parentNode))) { + return false; + } + } + + return true; + }; + + const getAllReadableNodes = (): NodeWithRect[] => { + if (!document.body) throw new Error("Page failed to load"); + const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { + acceptNode(node) { + if (doesNodePassHeuristics(node)) { + return NodeFilter.FILTER_ACCEPT; + } else { + return NodeFilter.FILTER_SKIP; + } + }, + }); + + const readableNodes = []; + + while (treeWalker.nextNode()) { + readableNodes.push(treeWalker.currentNode as ReadableNode); + } + + /* + *

hello

world

+ * table is already included in the parent of the first p tag + */ + + const parentsForReadableNodes = readableNodes.map(findHighestDirectParentOfReadableNode); + const listWithOnlyParents: HTMLElement[] = []; + // find unique nodes in the parent list, a unique node is a node that is not a child of any other node in the list + for (let i = 0; i < parentsForReadableNodes.length; i++) { + const node = parentsForReadableNodes[i]; + const hasParentInList = parentsForReadableNodes.find((otherNode, idx) => { + if (i === idx) return false; + return otherNode.contains(node); + }); + listWithOnlyParents.push(hasParentInList ? hasParentInList : node); + } + + const uniqueParents = Array.from(new Set(listWithOnlyParents)); + + return uniqueParents.map((node) => { + return { + node, + rect: node.getBoundingClientRect(), + }; + }); + }; + + const distanceFunction = (a: NodeWithRect, b: NodeWithRect) => { + // we make two assumptions here which are fine to make for rects returned from getBoundingClientRect + // 1. rects are upright and not rotated + // 2. If two rects intersect, we assume distance to be 0 + let dx = 0; + let dy = 0; + const rect1 = a.rect; + const rect2 = b.rect; + // Calculate the horizontal distance + if (rect1.x + rect1.width < rect2.x) { + dx = rect2.x - (rect1.x + rect1.width); + } else if (rect2.x + rect2.width < rect1.x) { + dx = rect1.x - (rect2.x + rect2.width); + } + + // Calculate the vertical distance + if (rect1.y + rect1.height < rect2.y) { + dy = rect2.y - (rect1.y + rect1.height); + } else if (rect2.y + rect2.height < rect1.y) { + dy = rect1.y - (rect2.y + rect2.height); + } + + const distance = Math.sqrt(dx * dx + dy * dy); + // Return the Euclidean distance + return distance; + }; + /** + * Clusters nodes using dbscan + */ + const clusterReadableNodes = (nodes: NodeWithRect[]) => { + const { clusters } = DBSCAN({ + dataset: nodes, + epsilon: 28, + minimumPoints: 1, + distanceFunction, + }); + + return clusters; + }; + + const totalTextLength = (cluster: number[]) => { + return cluster + .map((t) => readableNodes[t].node.innerText?.replaceAll(/ {2}|\r\n|\n|\r/gm, "")) + .join("").length; + }; + + const approximatelyEqual = (a: number, b: number, epsilon = 1) => { + return Math.abs(a - b) < epsilon; + }; + + const getClusterBounds = (cluster: number[]) => { + const leftMostPoint = Math.min(...cluster.map((c) => readableNodes[c].rect.x)); + const topMostPoint = Math.min(...cluster.map((c) => readableNodes[c].rect.y)); + const rightMostPoint = Math.max( + ...cluster.map((c) => readableNodes[c].rect.x + readableNodes[c].rect.width) + ); + const bottomMostPoint = Math.max( + ...cluster.map((c) => readableNodes[c].rect.y + readableNodes[c].rect.height) + ); + return { + // left most element + x: leftMostPoint, + y: topMostPoint, + width: rightMostPoint - leftMostPoint, + height: bottomMostPoint - topMostPoint, + }; + }; + + const round = (num: number, decimalPlaces = 2) => { + const factor = Math.pow(10, decimalPlaces); + return Math.round(num * factor) / factor; + }; + + /** minimum distance to center of the screen */ + const clusterCentrality = (cluster: number[]) => { + const bounds = getClusterBounds(cluster); + const centerOfScreen = window.innerWidth / 2; + // the cluster contains the center of the screen + if (bounds.x < centerOfScreen && bounds.x + bounds.width > centerOfScreen) { + return 0; + } + + // the cluster is to the left of the screen + if (bounds.x + bounds.width < centerOfScreen) { + return centerOfScreen - (bounds.x + bounds.width); + } + + // the cluster is to the right of the screen + return bounds.x - centerOfScreen; + }; + /** measure of text share that belong to the cluster */ + const percentageTextShare = (cluster: number[], totalLength: number) => { + // apply an exponentially increasing penalty for centrality per 100 pixels distance from center + + return round((totalTextLength(cluster) / totalLength) * 100); + }; + + const shouldMergeClusters = (clusterA: number[], clusterB: number[]) => { + const clusterABounds = getClusterBounds(clusterA); + const clusterBBounds = getClusterBounds(clusterB); + + // A cluster is horizontally aligned if the x and width are roughly equal + const isHorizontallyAligned = + approximatelyEqual(clusterABounds.x, clusterBBounds.x, 40) && + approximatelyEqual(clusterABounds.width, clusterBBounds.width, 40); + + if (!isHorizontallyAligned) return false; + + // check the y gap between the clusters + const higherCluster = clusterABounds.y < clusterBBounds.y ? clusterABounds : clusterBBounds; + const lowerCluster = clusterABounds.y < clusterBBounds.y ? clusterBBounds : clusterABounds; + const yGap = lowerCluster.y - (higherCluster.y + higherCluster.height); + + if (approximatelyEqual(yGap, 0, 100)) return true; + }; + + const findCriticalClusters = (clusters: number[][]) => { + // merge the clusters that have similar widths and x position + + let i = 0; + while (i < clusters.length) { + const cluster = clusters[i]; + for (let j = i + 1; j < clusters.length; j++) { + const otherCluster = clusters[j]; + if (shouldMergeClusters(cluster, otherCluster)) { + cluster.push(...otherCluster); + clusters.splice(j, 1); + j -= 1; + } + } + + i++; + } + + const totalText = totalTextLength(clusters.flat()); + + // sort in descending order of text share + const clusterWithMetrics = clusters.map((cluster) => { + const centrality = clusterCentrality(cluster); + return { + cluster, + centrality, + percentageTextShare: percentageTextShare(cluster, totalText), + }; + }); + + // if there is a dominant cluster with more than 60% text share, return that + const dominantCluster = clusterWithMetrics[0]?.percentageTextShare > 60; + if (dominantCluster) return [clusterWithMetrics[0].cluster]; + + // clusters are sorted by text share after applying a penalty for centrality + const sortedClusters = clusterWithMetrics.sort((a, b) => { + const penaltyForA = Math.pow(0.9, a.centrality / 100); + const penaltyForB = Math.pow(0.9, b.centrality / 100); + const adjustedTextShareA = a.percentageTextShare * penaltyForA; + const adjustedTextShareB = b.percentageTextShare * penaltyForB; + + return adjustedTextShareB - adjustedTextShareA; + }); + + // find all clusters that are similar to the largest cluster in terms of text share + // and see if they are enough to cover at least 60% of the text share + const largeTextShareClusters = sortedClusters.filter((c) => + approximatelyEqual(c.percentageTextShare, sortedClusters[0]?.percentageTextShare, 10) + ); + + const totalTextShareOfLargeClusters = largeTextShareClusters.reduce( + (acc, cluster) => acc + cluster.percentageTextShare, + 0 + ); + + if (totalTextShareOfLargeClusters > 60) { + return largeTextShareClusters.map((c) => c.cluster); + } + + // choose clusters till the text share is greater than 60% + let totalTextShare = 0; + const criticalClusters = []; + for (const cluster of sortedClusters) { + /** Ignore clusters with less than 2%*/ + if (cluster.percentageTextShare < 2) continue; + if (totalTextShare > 60) break; + criticalClusters.push(cluster.cluster); + totalTextShare += cluster.percentageTextShare; + } + + // if the total text share is less than 60% then return an empty array + // as this website should not be particularly useful for the web search anyways + // this should almost never happen on structured website with a lot of text + if (totalTextShare < 60) { + return []; + } + + return criticalClusters; + }; + + const allowListedAttributes = ["href", "src", "alt", "title", "class", "id"]; + function serializeHTMLElement(node: Element): SerializedHTMLElement { + return { + tagName: node.tagName.toLowerCase(), + attributes: allowListedAttributes.reduce( + (acc, attr) => { + const value = node.getAttribute(attr); + if (value) { + acc[attr] = value; + } + return acc; + }, + {} as Record + ), + content: Array.from(node.childNodes).map(serializeNode).filter(Boolean), + }; + } + + function serializeNode(node: Node): SerializedHTMLElement | string { + if (node.nodeType === 1) return serializeHTMLElement(node as Element); + else if (node.nodeType === 3) return node.textContent ?? ""; + else return ""; + } + + function getPageMetadata(): { + title: string; + siteName?: string; + author?: string; + description?: string; + createdAt?: string; + updatedAt?: string; + } { + const title = document.title ?? ""; + const siteName = + document.querySelector("meta[property='og:site_name']")?.getAttribute("content") ?? undefined; + const author = + document.querySelector("meta[name='author']")?.getAttribute("content") ?? undefined; + const description = + document.querySelector("meta[name='description']")?.getAttribute("content") ?? + document.querySelector("meta[property='og:description']")?.getAttribute("content") ?? + undefined; + const createdAt = + document.querySelector("meta[property='article:published_time']")?.getAttribute("content") ?? + document.querySelector("meta[name='date']")?.getAttribute("content") ?? + undefined; + const updatedAt = + document.querySelector("meta[property='article:modified_time']")?.getAttribute("content") ?? + undefined; + + return { title, siteName, author, description, createdAt, updatedAt }; + } + + const readableNodes = getAllReadableNodes(); + const clusters = clusterReadableNodes(readableNodes); + + const criticalClusters = findCriticalClusters(clusters); + + // filter readable nodes using the above information as well as heuristics + const filteredNodes = readableNodes.filter((_, idx) => { + return criticalClusters.some((cluster) => { + return cluster.includes(idx); + }); + }); + + const elements = filteredNodes + .filter( + (node, idx, nodes) => !nodes.slice(idx + 1).some((otherNode) => node.node === otherNode.node) + ) + .map(({ node }) => serializeHTMLElement(node)); + const metadata = getPageMetadata(); + return { ...metadata, elements }; +} diff --git a/src/lib/server/websearch/scrape/playwright.ts b/src/lib/server/websearch/scrape/playwright.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4b84dce450d4752c44ed1dee5a7e809394a9526 --- /dev/null +++ b/src/lib/server/websearch/scrape/playwright.ts @@ -0,0 +1,96 @@ +import { + chromium, + devices, + type Page, + type BrowserContextOptions, + type Response, + type Browser, +} from "playwright"; +import { PlaywrightBlocker } from "@cliqz/adblocker-playwright"; +import { env } from "$env/dynamic/private"; +import { logger } from "$lib/server/logger"; +import { onExit } from "$lib/server/exitHandler"; + +const blocker = + env.PLAYWRIGHT_ADBLOCKER === "true" + ? await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch) + .then((blker) => { + const mostBlocked = blker.blockFonts().blockMedias().blockFrames().blockImages(); + if (env.WEBSEARCH_JAVASCRIPT === "false") return mostBlocked.blockScripts(); + return mostBlocked; + }) + .catch((err) => { + logger.error(err, "Failed to initialize PlaywrightBlocker from prebuilt lists"); + return PlaywrightBlocker.empty(); + }) + : PlaywrightBlocker.empty(); + +let browserSingleton: Promise | undefined; +async function getBrowser() { + const browser = await chromium.launch({ headless: true }); + onExit(() => browser.close()); + browser.on("disconnected", () => { + logger.warn("Browser closed"); + browserSingleton = undefined; + }); + return browser; +} + +async function getPlaywrightCtx() { + if (!browserSingleton) browserSingleton = getBrowser(); + const browser = await browserSingleton; + + const device = devices["Desktop Chrome"]; + const options: BrowserContextOptions = { + ...device, + // Increasing width improves spatial clustering accuracy + screen: { + width: 3840, + height: 1080, + }, + viewport: { + width: 3840, + height: 1080, + }, + reducedMotion: "reduce", + acceptDownloads: false, + timezoneId: "America/New_York", + locale: "en-US", + }; + return browser.newContext(options); +} + +export async function withPage( + url: string, + callback: (page: Page, response?: Response) => Promise +): Promise { + const ctx = await getPlaywrightCtx(); + + try { + const page = await ctx.newPage(); + if (env.PLAYWRIGHT_ADBLOCKER === "true") { + await blocker.enableBlockingInPage(page); + } + + await page.route("**", (route, request) => { + const requestUrl = request.url(); + if (!requestUrl.startsWith("https://")) { + logger.warn(`Blocked request to: ${requestUrl}`); + return route.abort(); + } + return route.continue(); + }); + + const res = await page + .goto(url, { waitUntil: "load", timeout: parseInt(env.WEBSEARCH_TIMEOUT) }) + .catch(() => { + console.warn( + `Failed to load page within ${parseInt(env.WEBSEARCH_TIMEOUT) / 1000}s: ${url}` + ); + }); + + return await callback(page, res ?? undefined); + } finally { + await ctx.close(); + } +} diff --git a/src/lib/server/websearch/scrape/scrape.ts b/src/lib/server/websearch/scrape/scrape.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f12a439fe401fcb5c10bc0c5a1f9b3b870a0530 --- /dev/null +++ b/src/lib/server/websearch/scrape/scrape.ts @@ -0,0 +1,72 @@ +import type { WebSearchScrapedSource, WebSearchSource } from "$lib/types/WebSearch"; +import type { MessageWebSearchUpdate } from "$lib/types/MessageUpdate"; +import { withPage } from "./playwright"; + +import { spatialParser } from "./parser"; +import { htmlToMarkdownTree } from "../markdown/tree"; +import { timeout } from "$lib/utils/timeout"; +import { makeGeneralUpdate } from "../update"; +import { MetricsServer } from "$lib/server/metrics"; +import { logger } from "$lib/server/logger"; + +export const scrape = (maxCharsPerElem: number) => + async function* ( + source: WebSearchSource + ): AsyncGenerator { + try { + const startTime = Date.now(); + MetricsServer.getMetrics().webSearch.pageFetchCount.inc(); + + const page = await scrapeUrl(source.link, maxCharsPerElem); + + MetricsServer.getMetrics().webSearch.pageFetchDuration.observe(Date.now() - startTime); + + yield makeGeneralUpdate({ + message: "Browsing webpage", + args: [source.link], + }); + return { ...source, page }; + } catch (e) { + MetricsServer.getMetrics().webSearch.pageFetchCountError.inc(); + logger.error(e, `Error scraping webpage: ${source.link}`); + } + }; + +export async function scrapeUrl(url: string, maxCharsPerElem: number) { + return withPage(url, async (page, res) => { + if (!res) throw Error("Failed to load page"); + if (!res.ok()) throw Error(`Failed to load page: ${res.status()}`); + + // Check if it's a non-html content type that we can handle directly + // TODO: direct mappings to markdown can be added for markdown, csv and others + const contentType = res.headers()["content-type"] ?? ""; + if ( + contentType.includes("text/plain") || + contentType.includes("text/markdown") || + contentType.includes("application/json") || + contentType.includes("application/xml") || + contentType.includes("text/csv") + ) { + const title = await page.title(); + const content = await page.content(); + return { + title, + markdownTree: htmlToMarkdownTree( + title, + [{ tagName: "p", attributes: {}, content: [content] }], + maxCharsPerElem + ), + }; + } + + const scrapedOutput = await timeout(page.evaluate(spatialParser), 2000) + .then(({ elements, ...parsed }) => ({ + ...parsed, + markdownTree: htmlToMarkdownTree(parsed.title, elements, maxCharsPerElem), + })) + .catch((cause) => { + throw Error("Parsing failed", { cause }); + }); + return scrapedOutput; + }); +} diff --git a/src/lib/server/websearch/scrape/types.ts b/src/lib/server/websearch/scrape/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..f156703584778690ec3459f96cac011b91f87275 --- /dev/null +++ b/src/lib/server/websearch/scrape/types.ts @@ -0,0 +1,5 @@ +export interface SerializedHTMLElement { + tagName: string; + attributes: Record; + content: (SerializedHTMLElement | string)[]; +} diff --git a/src/lib/server/websearch/search/endpoints.ts b/src/lib/server/websearch/search/endpoints.ts new file mode 100644 index 0000000000000000000000000000000000000000..303ce28ca0b66ea889e8c7f8c5e95e588c1ddfdd --- /dev/null +++ b/src/lib/server/websearch/search/endpoints.ts @@ -0,0 +1,32 @@ +import { WebSearchProvider, type WebSearchSource } from "$lib/types/WebSearch"; +import { env } from "$env/dynamic/private"; +import searchSerper from "./endpoints/serper"; +import searchSerpApi from "./endpoints/serpApi"; +import searchSerpStack from "./endpoints/serpStack"; +import searchYouApi from "./endpoints/youApi"; +import searchWebLocal from "./endpoints/webLocal"; +import searchSearxng from "./endpoints/searxng"; +import searchSearchApi from "./endpoints/searchApi"; +import searchBing from "./endpoints/bing"; + +export function getWebSearchProvider() { + if (env.YDC_API_KEY) return WebSearchProvider.YOU; + if (env.SEARXNG_QUERY_URL) return WebSearchProvider.SEARXNG; + if (env.BING_SUBSCRIPTION_KEY) return WebSearchProvider.BING; + return WebSearchProvider.GOOGLE; +} + +/** Searches the web using the first available provider, based on the env */ +export async function searchWeb(query: string): Promise { + if (env.USE_LOCAL_WEBSEARCH) return searchWebLocal(query); + if (env.SEARXNG_QUERY_URL) return searchSearxng(query); + if (env.SERPER_API_KEY) return searchSerper(query); + if (env.YDC_API_KEY) return searchYouApi(query); + if (env.SERPAPI_KEY) return searchSerpApi(query); + if (env.SERPSTACK_API_KEY) return searchSerpStack(query); + if (env.SEARCHAPI_KEY) return searchSearchApi(query); + if (env.BING_SUBSCRIPTION_KEY) return searchBing(query); + throw new Error( + "No configuration found for web search. Please set USE_LOCAL_WEBSEARCH, SEARXNG_QUERY_URL, SERPER_API_KEY, YDC_API_KEY, SERPSTACK_API_KEY, or SEARCHAPI_KEY in your environment variables." + ); +} diff --git a/src/lib/server/websearch/search/endpoints/bing.ts b/src/lib/server/websearch/search/endpoints/bing.ts new file mode 100644 index 0000000000000000000000000000000000000000..20b28ab4999cc23fb2b1dc1baf993de6d8a58e29 --- /dev/null +++ b/src/lib/server/websearch/search/endpoints/bing.ts @@ -0,0 +1,38 @@ +import type { WebSearchSource } from "$lib/types/WebSearch"; +import { env } from "$env/dynamic/private"; + +export default async function search(query: string): Promise { + // const params = { + // q: query, + // // You can add other parameters if needed, like 'count', 'offset', etc. + // }; + + const response = await fetch( + "https://api.bing.microsoft.com/v7.0/search" + "?q=" + encodeURIComponent(query), + { + method: "GET", + headers: { + "Ocp-Apim-Subscription-Key": env.BING_SUBSCRIPTION_KEY, + "Content-type": "application/json", + }, + } + ); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const data = (await response.json()) as Record; + + if (!response.ok) { + throw new Error( + data["message"] ?? `Bing API returned error code ${response.status} - ${response.statusText}` + ); + } + + // Adapt the data structure from the Bing response to match the WebSearchSource type + const webPages = data["webPages"]?.["value"] ?? []; + return webPages.map((page: any) => ({ + title: page.name, + link: page.url, + text: page.snippet, + displayLink: page.displayUrl, + })); +} diff --git a/src/lib/server/websearch/search/endpoints/searchApi.ts b/src/lib/server/websearch/search/endpoints/searchApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..1108bb89a4f422fcc5fa5c7f6a0ff564cf549fcf --- /dev/null +++ b/src/lib/server/websearch/search/endpoints/searchApi.ts @@ -0,0 +1,26 @@ +import { env } from "$env/dynamic/private"; +import type { WebSearchSource } from "$lib/types/WebSearch"; + +export default async function search(query: string): Promise { + const response = await fetch( + `https://www.searchapi.io/api/v1/search?engine=google&hl=en&gl=us&q=${query}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${env.SEARCHAPI_KEY}`, + "Content-type": "application/json", + }, + } + ); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const data = (await response.json()) as Record; + + if (!response.ok) { + throw new Error( + data["message"] ?? `SearchApi returned error code ${response.status} - ${response.statusText}` + ); + } + + return data["organic_results"] ?? []; +} diff --git a/src/lib/server/websearch/search/endpoints/searxng.ts b/src/lib/server/websearch/search/endpoints/searxng.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ed1af379e1d73c20d6b4767f5898d0d6205ff17 --- /dev/null +++ b/src/lib/server/websearch/search/endpoints/searxng.ts @@ -0,0 +1,37 @@ +import { env } from "$env/dynamic/private"; +import { logger } from "$lib/server/logger"; +import type { WebSearchSource } from "$lib/types/WebSearch"; +import { isURL } from "$lib/utils/isUrl"; + +export default async function searchSearxng(query: string): Promise { + const abortController = new AbortController(); + setTimeout(() => abortController.abort(), 10000); + + // Insert the query into the URL template + let url = env.SEARXNG_QUERY_URL.replace("", query); + + // Check if "&format=json" already exists in the URL + if (!url.includes("&format=json")) { + url += "&format=json"; + } + + // Call the URL to return JSON data + const jsonResponse = await fetch(url, { + signal: abortController.signal, + }) + .then((response) => response.json() as Promise<{ results: { url: string }[] }>) + .catch((error) => { + logger.error(error, "Failed to fetch or parse JSON"); + throw new Error("Failed to fetch or parse JSON", { cause: error }); + }); + + // Extract 'url' elements from the JSON response and trim to the top 5 URLs + const urls = jsonResponse.results.slice(0, 5).map((item) => item.url); + + if (!urls.length) { + throw new Error(`Response doesn't contain any "url" elements`); + } + + // Map URLs to the correct object shape + return urls.filter(isURL).map((link) => ({ link })); +} diff --git a/src/lib/server/websearch/search/endpoints/serpApi.ts b/src/lib/server/websearch/search/endpoints/serpApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..adef629d8717ec4f462d904134f7302cfd8f93d6 --- /dev/null +++ b/src/lib/server/websearch/search/endpoints/serpApi.ts @@ -0,0 +1,25 @@ +import { env } from "$env/dynamic/private"; +import { getJson, type GoogleParameters } from "serpapi"; +import type { WebSearchSource } from "$lib/types/WebSearch"; +import { isURL } from "$lib/utils/isUrl"; + +type SerpApiResponse = { + organic_results: { + link: string; + }[]; +}; + +export default async function searchWebSerpApi(query: string): Promise { + const params = { + q: query, + hl: "en", + gl: "us", + google_domain: "google.com", + api_key: env.SERPAPI_KEY, + } satisfies GoogleParameters; + + // Show result as JSON + const response = (await getJson("google", params)) as unknown as SerpApiResponse; + + return response.organic_results.filter(({ link }) => isURL(link)); +} diff --git a/src/lib/server/websearch/search/endpoints/serpStack.ts b/src/lib/server/websearch/search/endpoints/serpStack.ts new file mode 100644 index 0000000000000000000000000000000000000000..fdb5734126f0e96bb65f90f85c16d48da30d957e --- /dev/null +++ b/src/lib/server/websearch/search/endpoints/serpStack.ts @@ -0,0 +1,35 @@ +import { env } from "$env/dynamic/private"; +import { isURL } from "$lib/utils/isUrl"; +import type { WebSearchSource } from "$lib/types/WebSearch"; + +type SerpStackResponse = { + organic_results: { + title: string; + url: string; + snippet?: string; + }[]; + error?: string; +}; + +export default async function searchSerpStack(query: string): Promise { + const response = await fetch( + `http://api.serpstack.com/search?access_key=${env.SERPSTACK_API_KEY}&query=${query}&hl=en&gl=us`, + { headers: { "Content-type": "application/json; charset=UTF-8" } } + ); + + const data = (await response.json()) as SerpStackResponse; + + if (!response.ok) { + throw new Error( + data.error ?? `SerpStack API returned error code ${response.status} - ${response.statusText}` + ); + } + + return data.organic_results + .filter(({ url }) => isURL(url)) + .map(({ title, url, snippet }) => ({ + title, + link: url, + text: snippet ?? "", + })); +} diff --git a/src/lib/server/websearch/search/endpoints/serper.ts b/src/lib/server/websearch/search/endpoints/serper.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a3223990833a567f9a16eef2688d0046ab4ace0 --- /dev/null +++ b/src/lib/server/websearch/search/endpoints/serper.ts @@ -0,0 +1,31 @@ +import { env } from "$env/dynamic/private"; +import type { WebSearchSource } from "$lib/types/WebSearch"; + +export default async function search(query: string): Promise { + const params = { + q: query, + hl: "en", + gl: "us", + }; + + const response = await fetch("https://google.serper.dev/search", { + method: "POST", + body: JSON.stringify(params), + headers: { + "x-api-key": env.SERPER_API_KEY, + "Content-type": "application/json", + }, + }); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const data = (await response.json()) as Record; + + if (!response.ok) { + throw new Error( + data["message"] ?? + `Serper API returned error code ${response.status} - ${response.statusText}` + ); + } + + return data["organic"] ?? []; +} diff --git a/src/lib/server/websearch/search/endpoints/webLocal.ts b/src/lib/server/websearch/search/endpoints/webLocal.ts new file mode 100644 index 0000000000000000000000000000000000000000..a847707675c5636da6d141b53e25073dcdefca9e --- /dev/null +++ b/src/lib/server/websearch/search/endpoints/webLocal.ts @@ -0,0 +1,35 @@ +import { JSDOM, VirtualConsole } from "jsdom"; +import { isURL } from "$lib/utils/isUrl"; +import type { WebSearchSource } from "$lib/types/WebSearch"; + +export default async function searchWebLocal(query: string): Promise { + const abortController = new AbortController(); + setTimeout(() => abortController.abort(), 10000); + + const htmlString = await fetch( + "https://www.google.com/search?hl=en&q=" + encodeURIComponent(query), + { signal: abortController.signal } + ) + .then((response) => response.text()) + .catch(); + + const virtualConsole = new VirtualConsole(); + virtualConsole.on("error", () => {}); // No-op to skip console errors. + const document = new JSDOM(htmlString ?? "", { virtualConsole }).window.document; + + // get all links + const links = document.querySelectorAll("a"); + if (!links.length) throw new Error(`webpage doesn't have any "a" element`); + + // take url that start wirth /url?q= + // and do not contain google.com links + // and strip them up to '&sa=' + const linksHref = Array.from(links) + .map((el) => el.href) + .filter((link) => link.startsWith("/url?q=") && !link.includes("google.com/")) + .map((link) => link.slice("/url?q=".length, link.indexOf("&sa="))) + .filter(isURL); + + // remove duplicate links and map links to the correct object shape + return [...new Set(linksHref)].map((link) => ({ link })); +} diff --git a/src/lib/server/websearch/search/endpoints/youApi.ts b/src/lib/server/websearch/search/endpoints/youApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..0173000d77d102cde876b5b59e779f88c98d6f69 --- /dev/null +++ b/src/lib/server/websearch/search/endpoints/youApi.ts @@ -0,0 +1,41 @@ +import { env } from "$env/dynamic/private"; +import { isURL } from "$lib/utils/isUrl"; +import type { WebSearchSource } from "$lib/types/WebSearch"; + +interface YouWebSearch { + hits: YouSearchHit[]; + latency: number; +} + +interface YouSearchHit { + url: string; + title: string; + description: string; + snippets: string[]; +} + +export default async function searchWebYouApi(query: string): Promise { + const response = await fetch(`https://api.ydc-index.io/search?query=${query}`, { + method: "GET", + headers: { + "X-API-Key": env.YDC_API_KEY, + "Content-type": "application/json; charset=UTF-8", + }, + }); + + if (!response.ok) { + throw new Error(`You.com API returned error code ${response.status} - ${response.statusText}`); + } + + const data = (await response.json()) as YouWebSearch; + const formattedResultsWithSnippets = data.hits + .filter(({ url }) => isURL(url)) + .map(({ title, url, snippets }) => ({ + title, + link: url, + text: snippets?.join("\n") || "", + })) + .sort((a, b) => b.text.length - a.text.length); // desc order by text length + + return formattedResultsWithSnippets; +} diff --git a/src/lib/server/websearch/search/generateQuery.ts b/src/lib/server/websearch/search/generateQuery.ts new file mode 100644 index 0000000000000000000000000000000000000000..96c3958bbeedc669fa8ffa6d1b9d587742fa1881 --- /dev/null +++ b/src/lib/server/websearch/search/generateQuery.ts @@ -0,0 +1,70 @@ +import type { Message } from "$lib/types/Message"; +import { format } from "date-fns"; +import type { EndpointMessage } from "../../endpoints/endpoints"; +import { generateFromDefaultEndpoint } from "../../generateFromDefaultEndpoint"; +import { getReturnFromGenerator } from "$lib/utils/getReturnFromGenerator"; +import { smallModel } from "$lib/server/models"; +import type { Tool } from "$lib/types/Tool"; +import { getToolOutput } from "$lib/server/tools/getToolOutput"; + +export async function generateQuery(messages: Message[]) { + const currentDate = format(new Date(), "MMMM d, yyyy"); + + if (smallModel.tools) { + const webSearchTool = { + name: "web_search", + description: "Search the web for information", + inputs: [ + { + name: "query", + type: "str", + description: "The query to search the web for", + paramType: "required", + }, + ], + } as unknown as Tool; + + const endpoint = await smallModel.getEndpoint(); + const query = await getToolOutput({ + messages, + preprompt: `The user wants you to search the web for information. Give a relevant google search query to answer the question. Answer with only the query. Today is ${currentDate}`, + tool: webSearchTool, + endpoint, + }); + + if (query) { + return query; + } + } + + const userMessages = messages.filter(({ from }) => from === "user"); + const previousUserMessages = userMessages.slice(0, -1); + + const lastMessage = userMessages.slice(-1)[0]; + + const convQuery: Array = [ + { + from: "user", + content: + (previousUserMessages.length > 0 + ? `Previous questions: \n${previousUserMessages + .map(({ content }) => `- ${content}`) + .join("\n")}` + : "") + + "\n\nCurrent Question: " + + lastMessage.content, + }, + ]; + + const webQuery = await getReturnFromGenerator( + generateFromDefaultEndpoint({ + messages: convQuery, + preprompt: `The user wants you to search the web for information. Give a relevant google search query to answer the question. Answer with only the query. Today is ${currentDate}. The conversation follows: \n`, + generateSettings: { + max_new_tokens: 30, + }, + }) + ); + + return webQuery.trim(); +} diff --git a/src/lib/server/websearch/search/search.ts b/src/lib/server/websearch/search/search.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f232a0ea9826c9581685d79348661adac826dff --- /dev/null +++ b/src/lib/server/websearch/search/search.ts @@ -0,0 +1,82 @@ +import type { WebSearchSource } from "$lib/types/WebSearch"; +import type { Message } from "$lib/types/Message"; +import type { Assistant } from "$lib/types/Assistant"; +import { getWebSearchProvider, searchWeb } from "./endpoints"; +import { generateQuery } from "./generateQuery"; +import { isURLStringLocal } from "$lib/server/isURLLocal"; +import { isURL } from "$lib/utils/isUrl"; + +import z from "zod"; +import JSON5 from "json5"; +import { env } from "$env/dynamic/private"; +import { makeGeneralUpdate } from "../update"; +import type { MessageWebSearchUpdate } from "$lib/types/MessageUpdate"; + +const listSchema = z.array(z.string()).default([]); +const allowList = listSchema.parse(JSON5.parse(env.WEBSEARCH_ALLOWLIST)); +const blockList = listSchema.parse(JSON5.parse(env.WEBSEARCH_BLOCKLIST)); + +export async function* search( + messages: Message[], + ragSettings?: Assistant["rag"], + query?: string +): AsyncGenerator< + MessageWebSearchUpdate, + { searchQuery: string; pages: WebSearchSource[] }, + undefined +> { + if (ragSettings && ragSettings?.allowedLinks.length > 0) { + yield makeGeneralUpdate({ message: "Using links specified in Assistant" }); + return { + searchQuery: "", + pages: await directLinksToSource(ragSettings.allowedLinks).then(filterByBlockList), + }; + } + + const searchQuery = query ?? (await generateQuery(messages)); + yield makeGeneralUpdate({ message: `Searching ${getWebSearchProvider()}`, args: [searchQuery] }); + + // handle the global and (optional) rag lists + if (ragSettings && ragSettings?.allowedDomains.length > 0) { + yield makeGeneralUpdate({ message: "Filtering on specified domains" }); + } + const filters = buildQueryFromSiteFilters( + [...(ragSettings?.allowedDomains ?? []), ...allowList], + blockList + ); + + const searchQueryWithFilters = `${filters} ${searchQuery}`; + const searchResults = await searchWeb(searchQueryWithFilters).then(filterByBlockList); + + return { + searchQuery: searchQueryWithFilters, + pages: searchResults, + }; +} + +// ---------- +// Utils +function filterByBlockList(results: WebSearchSource[]): WebSearchSource[] { + return results.filter((result) => !blockList.some((blocked) => result.link.includes(blocked))); +} + +function buildQueryFromSiteFilters(allow: string[], block: string[]) { + return ( + allow.map((item) => "site:" + item).join(" OR ") + + " " + + block.map((item) => "-site:" + item).join(" ") + ); +} + +async function directLinksToSource(links: string[]): Promise { + if (env.ENABLE_LOCAL_FETCH !== "true") { + const localLinks = await Promise.all(links.map(isURLStringLocal)); + links = links.filter((_, index) => !localLinks[index]); + } + + return links.filter(isURL).map((link) => ({ + link, + title: "", + text: [""], + })); +} diff --git a/src/lib/server/websearch/update.ts b/src/lib/server/websearch/update.ts new file mode 100644 index 0000000000000000000000000000000000000000..9a44779f799ca1550d8c88d06082606bede8ce45 --- /dev/null +++ b/src/lib/server/websearch/update.ts @@ -0,0 +1,45 @@ +import type { WebSearchSource } from "$lib/types/WebSearch"; +import { + MessageUpdateType, + MessageWebSearchUpdateType, + type MessageWebSearchErrorUpdate, + type MessageWebSearchFinishedUpdate, + type MessageWebSearchGeneralUpdate, + type MessageWebSearchSourcesUpdate, +} from "$lib/types/MessageUpdate"; + +export function makeGeneralUpdate( + update: Pick +): MessageWebSearchGeneralUpdate { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Update, + ...update, + }; +} + +export function makeErrorUpdate( + update: Pick +): MessageWebSearchErrorUpdate { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Error, + ...update, + }; +} + +export function makeSourcesUpdate(sources: WebSearchSource[]): MessageWebSearchSourcesUpdate { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Sources, + message: "sources", + sources: sources.map(({ link, title }) => ({ link, title })), + }; +} + +export function makeFinalAnswerUpdate(): MessageWebSearchFinishedUpdate { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Finished, + }; +} diff --git a/src/lib/shareConversation.ts b/src/lib/shareConversation.ts new file mode 100644 index 0000000000000000000000000000000000000000..7b2b9858c0badc6b3e0ccf3baf0a0045c318a4d6 --- /dev/null +++ b/src/lib/shareConversation.ts @@ -0,0 +1,33 @@ +import { base } from "$app/paths"; +import { ERROR_MESSAGES, error } from "$lib/stores/errors"; +import { share } from "./utils/share"; +import { page } from "$app/stores"; +import { get } from "svelte/store"; +import { getShareUrl } from "./utils/getShareUrl"; +export async function shareConversation(id: string, title: string) { + try { + if (id.length === 7) { + const url = get(page).url; + await share(getShareUrl(url, id), title, true); + } else { + const res = await fetch(`${base}/conversation/${id}/share`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!res.ok) { + error.set("Error while sharing conversation, try again."); + console.error("Error while sharing conversation: " + (await res.text())); + return; + } + + const { url } = await res.json(); + await share(url, title, true); + } + } catch (err) { + error.set(ERROR_MESSAGES.default); + console.error(err); + } +} diff --git a/src/lib/stores/errors.ts b/src/lib/stores/errors.ts new file mode 100644 index 0000000000000000000000000000000000000000..1022773bd97c122a46be0fc3f9e13c777ad2a04e --- /dev/null +++ b/src/lib/stores/errors.ts @@ -0,0 +1,9 @@ +import { writable } from "svelte/store"; + +export const ERROR_MESSAGES = { + default: "Oops, something went wrong.", + authOnly: "You have to be logged in.", + rateLimited: "You are sending too many messages. Try again later.", +}; + +export const error = writable(undefined); diff --git a/src/lib/stores/isAborted.ts b/src/lib/stores/isAborted.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed24aad14738e1d37e163f46eb8af7fa0fba004a --- /dev/null +++ b/src/lib/stores/isAborted.ts @@ -0,0 +1,3 @@ +import { writable } from "svelte/store"; + +export const isAborted = writable(false); diff --git a/src/lib/stores/loginModal.ts b/src/lib/stores/loginModal.ts new file mode 100644 index 0000000000000000000000000000000000000000..5bef13ad22df8963305c586e468e8babca1c8b26 --- /dev/null +++ b/src/lib/stores/loginModal.ts @@ -0,0 +1,3 @@ +import { writable } from "svelte/store"; + +export const loginModalOpen = writable(false); diff --git a/src/lib/stores/pendingMessage.ts b/src/lib/stores/pendingMessage.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a7387f393faac5c219d65880f9517b8ee3d6853 --- /dev/null +++ b/src/lib/stores/pendingMessage.ts @@ -0,0 +1,9 @@ +import { writable } from "svelte/store"; + +export const pendingMessage = writable< + | { + content: string; + files: File[]; + } + | undefined +>(); diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8e9615552e8e1e61752a950ef9f807b50329b35 --- /dev/null +++ b/src/lib/stores/settings.ts @@ -0,0 +1,107 @@ +import { browser } from "$app/environment"; +import { invalidate } from "$app/navigation"; +import { base } from "$app/paths"; +import { UrlDependency } from "$lib/types/UrlDependency"; +import type { ObjectId } from "mongodb"; +import { getContext, setContext } from "svelte"; +import { type Writable, writable, get } from "svelte/store"; + +type SettingsStore = { + shareConversationsWithModelAuthors: boolean; + hideEmojiOnSidebar: boolean; + ethicsModalAccepted: boolean; + ethicsModalAcceptedAt: Date | null; + activeModel: string; + customPrompts: Record; + recentlySaved: boolean; + assistants: Array; + tools?: Array; + disableStream: boolean; + directPaste: boolean; +}; + +type SettingsStoreWritable = Writable & { + instantSet: (settings: Partial) => Promise; +}; + +export function useSettingsStore() { + return getContext("settings"); +} + +export function createSettingsStore(initialValue: Omit) { + const baseStore = writable({ ...initialValue, recentlySaved: false }); + + let timeoutId: NodeJS.Timeout; + + async function setSettings(settings: Partial) { + baseStore.update((s) => ({ + ...s, + ...settings, + })); + + clearTimeout(timeoutId); + + if (browser) { + timeoutId = setTimeout(async () => { + await fetch(`${base}/settings`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...get(baseStore), + ...settings, + }), + }); + + invalidate(UrlDependency.ConversationList); + // set savedRecently to true for 3s + baseStore.update((s) => ({ + ...s, + recentlySaved: true, + })); + setTimeout(() => { + baseStore.update((s) => ({ + ...s, + recentlySaved: false, + })); + }, 3000); + invalidate(UrlDependency.ConversationList); + }, 300); + // debounce server calls by 300ms + } + } + async function instantSet(settings: Partial) { + baseStore.update((s) => ({ + ...s, + ...settings, + })); + + if (browser) { + await fetch(`${base}/settings`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...get(baseStore), + ...settings, + }), + }); + invalidate(UrlDependency.ConversationList); + } + } + + const newStore = { + subscribe: baseStore.subscribe, + set: setSettings, + instantSet, + update: (fn: (s: SettingsStore) => SettingsStore) => { + setSettings(fn(get(baseStore))); + }, + } satisfies SettingsStoreWritable; + + setContext("settings", newStore); + + return newStore; +} diff --git a/src/lib/stores/titleUpdate.ts b/src/lib/stores/titleUpdate.ts new file mode 100644 index 0000000000000000000000000000000000000000..6cefb303e525efe2651ae947a030af129fa01bac --- /dev/null +++ b/src/lib/stores/titleUpdate.ts @@ -0,0 +1,8 @@ +import { writable } from "svelte/store"; + +export interface TitleUpdate { + convId: string; + title: string; +} + +export default writable(null); diff --git a/src/lib/stores/webSearchParameters.ts b/src/lib/stores/webSearchParameters.ts new file mode 100644 index 0000000000000000000000000000000000000000..fd088a60621090930e9600c6086380afd2b412e8 --- /dev/null +++ b/src/lib/stores/webSearchParameters.ts @@ -0,0 +1,9 @@ +import { writable } from "svelte/store"; +export interface WebSearchParameters { + useSearch: boolean; + nItems: number; +} +export const webSearchParameters = writable({ + useSearch: false, + nItems: 5, +}); diff --git a/src/lib/switchTheme.ts b/src/lib/switchTheme.ts new file mode 100644 index 0000000000000000000000000000000000000000..fef1991bd9d2cfc24b846ee2ab603f9f814231a5 --- /dev/null +++ b/src/lib/switchTheme.ts @@ -0,0 +1,14 @@ +export function switchTheme() { + const { classList } = document.querySelector("html") as HTMLElement; + const metaTheme = document.querySelector('meta[name="theme-color"]') as HTMLMetaElement; + + if (classList.contains("dark")) { + classList.remove("dark"); + metaTheme.setAttribute("content", "rgb(249, 250, 251)"); + localStorage.theme = "light"; + } else { + classList.add("dark"); + metaTheme.setAttribute("content", "rgb(26, 36, 50)"); + localStorage.theme = "dark"; + } +} diff --git a/src/lib/types/AbortedGeneration.ts b/src/lib/types/AbortedGeneration.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe4c2824b4f3257bea71c3acacd65fcee0918188 --- /dev/null +++ b/src/lib/types/AbortedGeneration.ts @@ -0,0 +1,8 @@ +// Ideally shouldn't be needed, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 + +import type { Conversation } from "./Conversation"; +import type { Timestamps } from "./Timestamps"; + +export interface AbortedGeneration extends Timestamps { + conversationId: Conversation["_id"]; +} diff --git a/src/lib/types/Assistant.ts b/src/lib/types/Assistant.ts new file mode 100644 index 0000000000000000000000000000000000000000..805c38041d1424598b9b4f42f2436a04f815bdd2 --- /dev/null +++ b/src/lib/types/Assistant.ts @@ -0,0 +1,39 @@ +import type { ObjectId } from "mongodb"; +import type { User } from "./User"; +import type { Timestamps } from "./Timestamps"; +import type { ReviewStatus } from "./Review"; + +export interface Assistant extends Timestamps { + _id: ObjectId; + createdById: User["_id"] | string; // user id or session + createdByName?: User["username"]; + avatar?: string; + name: string; + description?: string; + modelId: string; + exampleInputs: string[]; + preprompt: string; + userCount?: number; + review: ReviewStatus; + rag?: { + allowAllDomains: boolean; + allowedDomains: string[]; + allowedLinks: string[]; + }; + generateSettings?: { + temperature?: number; + top_p?: number; + repetition_penalty?: number; + top_k?: number; + }; + dynamicPrompt?: boolean; + searchTokens: string[]; + last24HoursCount: number; + tools?: string[]; +} + +// eslint-disable-next-line no-shadow +export enum SortKey { + POPULAR = "popular", + TRENDING = "trending", +} diff --git a/src/lib/types/AssistantStats.ts b/src/lib/types/AssistantStats.ts new file mode 100644 index 0000000000000000000000000000000000000000..75576c0d7f2413541e431055a09d98ade251cfd7 --- /dev/null +++ b/src/lib/types/AssistantStats.ts @@ -0,0 +1,11 @@ +import type { Timestamps } from "./Timestamps"; +import type { Assistant } from "./Assistant"; + +export interface AssistantStats extends Timestamps { + assistantId: Assistant["_id"]; + date: { + at: Date; + span: "hour"; + }; + count: number; +} diff --git a/src/lib/types/ConvSidebar.ts b/src/lib/types/ConvSidebar.ts new file mode 100644 index 0000000000000000000000000000000000000000..679a41ba64191ff4f52991f55ee034b8a91e0ebc --- /dev/null +++ b/src/lib/types/ConvSidebar.ts @@ -0,0 +1,8 @@ +export interface ConvSidebar { + id: string; + title: string; + updatedAt: Date; + model?: string; + assistantId?: string; + avatarUrl?: string; +} diff --git a/src/lib/types/Conversation.ts b/src/lib/types/Conversation.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1f97978d90e18c8ae961296164a27d832638b2d --- /dev/null +++ b/src/lib/types/Conversation.ts @@ -0,0 +1,28 @@ +import type { ObjectId } from "mongodb"; +import type { Message } from "./Message"; +import type { Timestamps } from "./Timestamps"; +import type { User } from "./User"; +import type { Assistant } from "./Assistant"; + +export interface Conversation extends Timestamps { + _id: ObjectId; + + sessionId?: string; + userId?: User["_id"]; + + model: string; + embeddingModel: string; + + title: string; + rootMessageId?: Message["id"]; + messages: Message[]; + + meta?: { + fromShareId?: string; + }; + + preprompt?: string; + assistantId?: Assistant["_id"]; + + userAgent?: string; +} diff --git a/src/lib/types/ConversationStats.ts b/src/lib/types/ConversationStats.ts new file mode 100644 index 0000000000000000000000000000000000000000..93b8f1f21b286eef7bb248325c9cfccbf599f693 --- /dev/null +++ b/src/lib/types/ConversationStats.ts @@ -0,0 +1,13 @@ +import type { Timestamps } from "./Timestamps"; + +export interface ConversationStats extends Timestamps { + date: { + at: Date; + span: "day" | "week" | "month"; + field: "updatedAt" | "createdAt"; + }; + type: "conversation" | "message"; + /** _id => number of conversations/messages in the month */ + distinct: "sessionId" | "userId" | "userOrSessionId" | "_id"; + count: number; +} diff --git a/src/lib/types/Message.ts b/src/lib/types/Message.ts new file mode 100644 index 0000000000000000000000000000000000000000..171dc5ece9f2db494b4e02d062dd4795138b0231 --- /dev/null +++ b/src/lib/types/Message.ts @@ -0,0 +1,35 @@ +import type { MessageUpdate } from "./MessageUpdate"; +import type { Timestamps } from "./Timestamps"; +import type { WebSearch } from "./WebSearch"; +import type { v4 } from "uuid"; + +export type Message = Partial & { + from: "user" | "assistant" | "system"; + id: ReturnType; + content: string; + updates?: MessageUpdate[]; + webSearchId?: WebSearch["_id"]; // legacy version + webSearch?: WebSearch; + + reasoning?: string; + score?: -1 | 0 | 1; + /** + * Either contains the base64 encoded image data + * or the hash of the file stored on the server + **/ + files?: MessageFile[]; + interrupted?: boolean; + + // needed for conversation trees + ancestors?: Message["id"][]; + + // goes one level deep + children?: Message["id"][]; +}; + +export type MessageFile = { + type: "hash" | "base64"; + name: string; + value: string; + mime: string; +}; diff --git a/src/lib/types/MessageEvent.ts b/src/lib/types/MessageEvent.ts new file mode 100644 index 0000000000000000000000000000000000000000..9843cb29d8c9a902c6b399ff03b87cdcbcde12ca --- /dev/null +++ b/src/lib/types/MessageEvent.ts @@ -0,0 +1,8 @@ +import type { Session } from "./Session"; +import type { Timestamps } from "./Timestamps"; +import type { User } from "./User"; + +export interface MessageEvent extends Pick { + userId: User["_id"] | Session["sessionId"]; + ip?: string; +} diff --git a/src/lib/types/MessageUpdate.ts b/src/lib/types/MessageUpdate.ts new file mode 100644 index 0000000000000000000000000000000000000000..17a953289dc831524966f5f465c040123a047782 --- /dev/null +++ b/src/lib/types/MessageUpdate.ts @@ -0,0 +1,149 @@ +import type { WebSearchSource } from "$lib/types/WebSearch"; +import type { ToolCall, ToolResult } from "$lib/types/Tool"; + +export type MessageUpdate = + | MessageStatusUpdate + | MessageTitleUpdate + | MessageToolUpdate + | MessageWebSearchUpdate + | MessageStreamUpdate + | MessageFileUpdate + | MessageFinalAnswerUpdate + | MessageReasoningUpdate; + +export enum MessageUpdateType { + Status = "status", + Title = "title", + Tool = "tool", + WebSearch = "webSearch", + Stream = "stream", + File = "file", + FinalAnswer = "finalAnswer", + Reasoning = "reasoning", +} + +// Status +export enum MessageUpdateStatus { + Started = "started", + Error = "error", + Finished = "finished", + KeepAlive = "keepAlive", +} +export interface MessageStatusUpdate { + type: MessageUpdateType.Status; + status: MessageUpdateStatus; + message?: string; +} + +// Web search +export enum MessageWebSearchUpdateType { + Update = "update", + Error = "error", + Sources = "sources", + Finished = "finished", +} +export interface BaseMessageWebSearchUpdate { + type: MessageUpdateType.WebSearch; + subtype: TSubType; +} +export interface MessageWebSearchErrorUpdate + extends BaseMessageWebSearchUpdate { + message: string; + args?: string[]; +} +export interface MessageWebSearchGeneralUpdate + extends BaseMessageWebSearchUpdate { + message: string; + args?: string[]; +} +export interface MessageWebSearchSourcesUpdate + extends BaseMessageWebSearchUpdate { + message: string; + sources: WebSearchSource[]; +} +export type MessageWebSearchFinishedUpdate = + BaseMessageWebSearchUpdate; +export type MessageWebSearchUpdate = + | MessageWebSearchErrorUpdate + | MessageWebSearchGeneralUpdate + | MessageWebSearchSourcesUpdate + | MessageWebSearchFinishedUpdate; + +// Tool +export enum MessageToolUpdateType { + /** A request to call a tool alongside it's parameters */ + Call = "call", + /** The result of a tool call */ + Result = "result", + /** Error while running tool */ + Error = "error", + /** ETA update */ + ETA = "eta", +} + +interface MessageToolBaseUpdate { + type: MessageUpdateType.Tool; + subtype: TSubType; + uuid: string; +} +export interface MessageToolCallUpdate extends MessageToolBaseUpdate { + call: ToolCall; +} +export interface MessageToolResultUpdate + extends MessageToolBaseUpdate { + result: ToolResult; +} +export interface MessageToolErrorUpdate extends MessageToolBaseUpdate { + message: string; +} + +export interface MessageToolETAUpdate extends MessageToolBaseUpdate { + eta: number; +} + +export type MessageToolUpdate = + | MessageToolCallUpdate + | MessageToolResultUpdate + | MessageToolErrorUpdate + | MessageToolETAUpdate; + +// Everything else +export interface MessageTitleUpdate { + type: MessageUpdateType.Title; + title: string; +} +export interface MessageStreamUpdate { + type: MessageUpdateType.Stream; + token: string; +} + +export enum MessageReasoningUpdateType { + Stream = "stream", + Status = "status", +} + +export type MessageReasoningUpdate = MessageReasoningStreamUpdate | MessageReasoningStatusUpdate; + +export interface MessageReasoningStreamUpdate { + type: MessageUpdateType.Reasoning; + subtype: MessageReasoningUpdateType.Stream; + token: string; +} +export interface MessageReasoningStatusUpdate { + type: MessageUpdateType.Reasoning; + subtype: MessageReasoningUpdateType.Status; + status: string; +} + +export interface MessageFileUpdate { + type: MessageUpdateType.File; + name: string; + sha: string; + mime: string; +} +export interface MessageFinalAnswerUpdate { + type: MessageUpdateType.FinalAnswer; + text: string; + interrupted: boolean; + webSources?: { uri: string; title: string }[]; +} diff --git a/src/lib/types/MigrationResult.ts b/src/lib/types/MigrationResult.ts new file mode 100644 index 0000000000000000000000000000000000000000..aff17be6169b247e8dc2a76af164aef58ba9d7d4 --- /dev/null +++ b/src/lib/types/MigrationResult.ts @@ -0,0 +1,7 @@ +import type { ObjectId } from "mongodb"; + +export interface MigrationResult { + _id: ObjectId; + name: string; + status: "success" | "failure" | "ongoing"; +} diff --git a/src/lib/types/Model.ts b/src/lib/types/Model.ts new file mode 100644 index 0000000000000000000000000000000000000000..d69ffbdeb95c6d2b3e2e076365e6100d1c3c7283 --- /dev/null +++ b/src/lib/types/Model.ts @@ -0,0 +1,23 @@ +import type { BackendModel } from "$lib/server/models"; + +export type Model = Pick< + BackendModel, + | "id" + | "name" + | "displayName" + | "websiteUrl" + | "datasetName" + | "promptExamples" + | "parameters" + | "description" + | "logoUrl" + | "modelUrl" + | "tokenizer" + | "datasetUrl" + | "preprompt" + | "multimodal" + | "multimodalAcceptedMimetypes" + | "unlisted" + | "tools" + | "hasInferenceAPI" +>; diff --git a/src/lib/types/Report.ts b/src/lib/types/Report.ts new file mode 100644 index 0000000000000000000000000000000000000000..949f1b129d0b36c6533d1f9633e47fd133ebe7ac --- /dev/null +++ b/src/lib/types/Report.ts @@ -0,0 +1,12 @@ +import type { ObjectId } from "mongodb"; +import type { User } from "./User"; +import type { Assistant } from "./Assistant"; +import type { Timestamps } from "./Timestamps"; + +export interface Report extends Timestamps { + _id: ObjectId; + createdBy: User["_id"] | string; + object: "assistant" | "tool"; + contentId: Assistant["_id"]; + reason?: string; +} diff --git a/src/lib/types/Review.ts b/src/lib/types/Review.ts new file mode 100644 index 0000000000000000000000000000000000000000..48505f8b4541321a249b9db4ed7960373e3cbe7c --- /dev/null +++ b/src/lib/types/Review.ts @@ -0,0 +1,6 @@ +export enum ReviewStatus { + PRIVATE = "PRIVATE", + PENDING = "PENDING", + APPROVED = "APPROVED", + DENIED = "DENIED", +} diff --git a/src/lib/types/Semaphore.ts b/src/lib/types/Semaphore.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ea0d8ccb10bbce15b5be1f6dc7c7eff158d7091 --- /dev/null +++ b/src/lib/types/Semaphore.ts @@ -0,0 +1,5 @@ +import type { Timestamps } from "./Timestamps"; + +export interface Semaphore extends Timestamps { + key: string; +} diff --git a/src/lib/types/Session.ts b/src/lib/types/Session.ts new file mode 100644 index 0000000000000000000000000000000000000000..8fdb703e7e5c864239eba2c138f5e9a9a2050e18 --- /dev/null +++ b/src/lib/types/Session.ts @@ -0,0 +1,12 @@ +import type { ObjectId } from "bson"; +import type { Timestamps } from "./Timestamps"; +import type { User } from "./User"; + +export interface Session extends Timestamps { + _id: ObjectId; + sessionId: string; + userId: User["_id"]; + userAgent?: string; + ip?: string; + expiresAt: Date; +} diff --git a/src/lib/types/Settings.ts b/src/lib/types/Settings.ts new file mode 100644 index 0000000000000000000000000000000000000000..b66b0c8023bbadae460e9884eeb8349940b7a9d3 --- /dev/null +++ b/src/lib/types/Settings.ts @@ -0,0 +1,40 @@ +import { defaultModel } from "$lib/server/models"; +import type { Assistant } from "./Assistant"; +import type { Timestamps } from "./Timestamps"; +import type { User } from "./User"; + +export interface Settings extends Timestamps { + userId?: User["_id"]; + sessionId?: string; + + /** + * Note: Only conversations with this settings explicitly set to true should be shared. + * + * This setting is explicitly set to true when users accept the ethics modal. + * */ + shareConversationsWithModelAuthors: boolean; + ethicsModalAcceptedAt: Date | null; + activeModel: string; + hideEmojiOnSidebar?: boolean; + + // model name and system prompts + customPrompts?: Record; + + assistants?: Assistant["_id"][]; + tools?: string[]; + disableStream: boolean; + directPaste: boolean; +} + +export type SettingsEditable = Omit; +// TODO: move this to a constant file along with other constants +export const DEFAULT_SETTINGS = { + shareConversationsWithModelAuthors: true, + activeModel: defaultModel.id, + hideEmojiOnSidebar: false, + customPrompts: {}, + assistants: [], + tools: [], + disableStream: false, + directPaste: false, +} satisfies SettingsEditable; diff --git a/src/lib/types/SharedConversation.ts b/src/lib/types/SharedConversation.ts new file mode 100644 index 0000000000000000000000000000000000000000..99e4affb6e928d5ac06a7a3af295b1d950c1e955 --- /dev/null +++ b/src/lib/types/SharedConversation.ts @@ -0,0 +1,17 @@ +import type { Conversation } from "./Conversation"; + +export type SharedConversation = Pick< + Conversation, + | "model" + | "embeddingModel" + | "title" + | "rootMessageId" + | "messages" + | "preprompt" + | "assistantId" + | "createdAt" + | "updatedAt" +> & { + _id: string; + hash: string; +}; diff --git a/src/lib/types/Template.ts b/src/lib/types/Template.ts new file mode 100644 index 0000000000000000000000000000000000000000..d81569827c49f55ec258247f973c9fc130a431c0 --- /dev/null +++ b/src/lib/types/Template.ts @@ -0,0 +1,10 @@ +import type { Message } from "./Message"; +import type { Tool, ToolResult } from "./Tool"; + +export type ChatTemplateInput = { + messages: Pick[]; + preprompt?: string; + tools?: Tool[]; + toolResults?: ToolResult[]; + continueMessage?: boolean; +}; diff --git a/src/lib/types/Timestamps.ts b/src/lib/types/Timestamps.ts new file mode 100644 index 0000000000000000000000000000000000000000..12d1867d1be509310190df09d2392bfaa77d6500 --- /dev/null +++ b/src/lib/types/Timestamps.ts @@ -0,0 +1,4 @@ +export interface Timestamps { + createdAt: Date; + updatedAt: Date; +} diff --git a/src/lib/types/TokenCache.ts b/src/lib/types/TokenCache.ts new file mode 100644 index 0000000000000000000000000000000000000000..20c7463b1722437f5b5f7a25ab684c6736e6271a --- /dev/null +++ b/src/lib/types/TokenCache.ts @@ -0,0 +1,6 @@ +import type { Timestamps } from "./Timestamps"; + +export interface TokenCache extends Timestamps { + tokenHash: string; // sha256 of the bearer token + userId: string; // the matching hf user id +} diff --git a/src/lib/types/Tool.ts b/src/lib/types/Tool.ts new file mode 100644 index 0000000000000000000000000000000000000000..423251d7268a32756e6cda28a4382da07c120ec3 --- /dev/null +++ b/src/lib/types/Tool.ts @@ -0,0 +1,187 @@ +import type { ObjectId } from "mongodb"; +import type { User } from "./User"; +import type { Timestamps } from "./Timestamps"; +import type { BackendToolContext } from "$lib/server/tools"; +import type { MessageUpdate } from "./MessageUpdate"; +import { z } from "zod"; +import type { ReviewStatus } from "./Review"; + +export const ToolColor = z.union([ + z.literal("purple"), + z.literal("blue"), + z.literal("green"), + z.literal("yellow"), + z.literal("red"), +]); + +export const ToolIcon = z.union([ + z.literal("wikis"), + z.literal("tools"), + z.literal("camera"), + z.literal("code"), + z.literal("email"), + z.literal("cloud"), + z.literal("terminal"), + z.literal("game"), + z.literal("chat"), + z.literal("speaker"), + z.literal("video"), +]); + +export const ToolOutputComponents = z + .string() + .toLowerCase() + .pipe( + z.union([ + z.literal("textbox"), + z.literal("markdown"), + z.literal("image"), + z.literal("gallery"), + z.literal("number"), + z.literal("audio"), + z.literal("video"), + z.literal("file"), + z.literal("json"), + ]) + ); + +export type ToolOutputComponents = z.infer; + +export type ToolLogoColor = z.infer; +export type ToolLogoIcon = z.infer; + +export type ToolIOType = "str" | "int" | "float" | "bool" | "file"; + +export type ToolInputRequired = { + paramType: "required"; + name: string; + description?: string; +}; + +export type ToolInputOptional = { + paramType: "optional"; + name: string; + description?: string; + default: string | number | boolean; +}; + +export type ToolInputFixed = { + paramType: "fixed"; + name: string; + value: string | number | boolean; +}; + +type ToolInputBase = ToolInputRequired | ToolInputOptional | ToolInputFixed; + +export type ToolInputFile = ToolInputBase & { + type: "file"; + mimeTypes: string; +}; + +export type ToolInputSimple = ToolInputBase & { + type: Exclude; +}; + +export type ToolInput = ToolInputFile | ToolInputSimple; + +export interface BaseTool { + _id: ObjectId; + + name: string; // name that will be shown to the AI + + baseUrl?: string; // namespace for the tool + endpoint: string | null; // endpoint to call in gradio, if null we expect to override this function in code + outputComponent: string | null; // Gradio component type to use for the output + outputComponentIdx: number | null; // index of the output component + + inputs: Array; + showOutput: boolean; // show output in chat or not + + call: BackendCall; + + // for displaying in the UI + displayName: string; + color: ToolLogoColor; + icon: ToolLogoIcon; + description: string; +} + +export interface ConfigTool extends BaseTool { + type: "config"; + isOnByDefault?: true; + isLocked?: true; + isHidden?: true; +} + +export interface CommunityTool extends BaseTool, Timestamps { + type: "community"; + + createdById: User["_id"] | string; // user id or session + createdByName?: User["username"]; + + // used to compute popular & trending + useCount: number; + last24HoursUseCount: number; + + review: ReviewStatus; + searchTokens: string[]; +} + +// no call function in db +export type CommunityToolDB = Omit; + +export type CommunityToolEditable = Omit< + CommunityToolDB, + | "_id" + | "useCount" + | "last24HoursUseCount" + | "createdById" + | "createdByName" + | "review" + | "searchTokens" + | "type" + | "createdAt" + | "updatedAt" +>; + +export type Tool = ConfigTool | CommunityTool; + +export type ToolFront = Pick< + Tool, + "type" | "name" | "displayName" | "description" | "color" | "icon" +> & { + _id: string; + isOnByDefault: boolean; + isLocked: boolean; + mimeTypes: string[]; + timeToUseMS?: number; +}; + +export enum ToolResultStatus { + Success = "success", + Error = "error", +} +export interface ToolResultSuccess { + status: ToolResultStatus.Success; + call: ToolCall; + outputs: Record[]; + display?: boolean; +} +export interface ToolResultError { + status: ToolResultStatus.Error; + call: ToolCall; + message: string; + display?: boolean; +} +export type ToolResult = ToolResultSuccess | ToolResultError; + +export interface ToolCall { + name: string; + parameters: Record; +} + +export type BackendCall = ( + params: Record, + context: BackendToolContext, + uuid: string +) => AsyncGenerator, undefined>; diff --git a/src/lib/types/UrlDependency.ts b/src/lib/types/UrlDependency.ts new file mode 100644 index 0000000000000000000000000000000000000000..dca26f87f494d42c15d5f6079a0a5cacd81e59a9 --- /dev/null +++ b/src/lib/types/UrlDependency.ts @@ -0,0 +1,5 @@ +/* eslint-disable no-shadow */ +export enum UrlDependency { + ConversationList = "conversation:list", + Conversation = "conversation", +} diff --git a/src/lib/types/User.ts b/src/lib/types/User.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f300c588885f66835e0ae224539dcc9040a7850 --- /dev/null +++ b/src/lib/types/User.ts @@ -0,0 +1,14 @@ +import type { ObjectId } from "mongodb"; +import type { Timestamps } from "./Timestamps"; + +export interface User extends Timestamps { + _id: ObjectId; + + username?: string; + name: string; + email?: string; + avatarUrl: string | undefined; + hfUserId: string; + isAdmin?: boolean; + isEarlyAccess?: boolean; +} diff --git a/src/lib/types/WebSearch.ts b/src/lib/types/WebSearch.ts new file mode 100644 index 0000000000000000000000000000000000000000..7fddc3a99ae24877f3babb583a73663bcc9caca3 --- /dev/null +++ b/src/lib/types/WebSearch.ts @@ -0,0 +1,49 @@ +import type { ObjectId } from "mongodb"; +import type { Conversation } from "./Conversation"; +import type { Timestamps } from "./Timestamps"; +import type { HeaderElement } from "$lib/server/websearch/markdown/types"; + +export interface WebSearch extends Timestamps { + _id?: ObjectId; + convId?: Conversation["_id"]; + + prompt: string; + + searchQuery: string; + results: WebSearchSource[]; + contextSources: WebSearchUsedSource[]; +} + +export interface WebSearchSource { + title?: string; + link: string; +} +export interface WebSearchScrapedSource extends WebSearchSource { + page: WebSearchPage; +} +export interface WebSearchPage { + title: string; + siteName?: string; + author?: string; + description?: string; + createdAt?: string; + modifiedAt?: string; + markdownTree: HeaderElement; +} + +export interface WebSearchUsedSource extends WebSearchScrapedSource { + context: string; +} + +export type WebSearchMessageSources = { + type: "sources"; + sources: WebSearchSource[]; +}; + +// eslint-disable-next-line no-shadow +export enum WebSearchProvider { + GOOGLE = "Google", + YOU = "You.com", + SEARXNG = "SearXNG", + BING = "Bing", +} diff --git a/src/lib/utils/chunk.ts b/src/lib/utils/chunk.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d8f924eba449978957a62c39c7406f819edf49a --- /dev/null +++ b/src/lib/utils/chunk.ts @@ -0,0 +1,33 @@ +/** + * Chunk array into arrays of length at most `chunkSize` + * + * @param chunkSize must be greater than or equal to 1 + */ +export function chunk(arr: T, chunkSize: number): T[] { + if (isNaN(chunkSize) || chunkSize < 1) { + throw new RangeError("Invalid chunk size: " + chunkSize); + } + + if (!arr.length) { + return []; + } + + /// Small optimization to not chunk buffers unless needed + if (arr.length <= chunkSize) { + return [arr]; + } + + return range(Math.ceil(arr.length / chunkSize)).map((i) => { + return arr.slice(i * chunkSize, (i + 1) * chunkSize); + }) as T[]; +} + +function range(n: number, b?: number): number[] { + return b + ? Array(b - n) + .fill(0) + .map((_, i) => n + i) + : Array(n) + .fill(0) + .map((_, i) => i); +} diff --git a/src/lib/utils/cookiesAreEnabled.ts b/src/lib/utils/cookiesAreEnabled.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5bc92c29335d344c1fde2647c823765f05c4592 --- /dev/null +++ b/src/lib/utils/cookiesAreEnabled.ts @@ -0,0 +1,13 @@ +import { browser } from "$app/environment"; + +export function cookiesAreEnabled(): boolean { + if (!browser) return false; + if (navigator.cookieEnabled) return navigator.cookieEnabled; + + // Create cookie + document.cookie = "cookietest=1"; + const ret = document.cookie.indexOf("cookietest=") != -1; + // Delete cookie + document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT"; + return ret; +} diff --git a/src/lib/utils/debounce.ts b/src/lib/utils/debounce.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8b7560a63e15955496e4a9c425f43504249cdec --- /dev/null +++ b/src/lib/utils/debounce.ts @@ -0,0 +1,17 @@ +/** + * A debounce function that works in both browser and Nodejs. + * For pure Nodejs work, prefer the `Debouncer` class. + */ +export function debounce( + callback: (...rest: T) => unknown, + limit: number +): (...rest: T) => void { + let timer: ReturnType; + + return function (...rest) { + clearTimeout(timer); + timer = setTimeout(() => { + callback(...rest); + }, limit); + }; +} diff --git a/src/lib/utils/deepestChild.ts b/src/lib/utils/deepestChild.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac6ed1d1dd64e6f3a8bbd559887de77b3e49f8c3 --- /dev/null +++ b/src/lib/utils/deepestChild.ts @@ -0,0 +1,6 @@ +export function deepestChild(el: HTMLElement): HTMLElement { + if (el.lastElementChild && el.lastElementChild.nodeType !== Node.TEXT_NODE) { + return deepestChild(el.lastElementChild as HTMLElement); + } + return el; +} diff --git a/src/lib/utils/file2base64.ts b/src/lib/utils/file2base64.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b5dbc66ec3197e19cc3789fda0f0b07a1363b80 --- /dev/null +++ b/src/lib/utils/file2base64.ts @@ -0,0 +1,14 @@ +const file2base64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + const dataUrl = reader.result as string; + const base64 = dataUrl.split(",")[1]; + resolve(base64); + }; + reader.onerror = (error) => reject(error); + }); +}; + +export default file2base64; diff --git a/src/lib/utils/formatUserCount.ts b/src/lib/utils/formatUserCount.ts new file mode 100644 index 0000000000000000000000000000000000000000..27087d7a8ad3e5b36cb2fc800385d27cf3ebdb0a --- /dev/null +++ b/src/lib/utils/formatUserCount.ts @@ -0,0 +1,37 @@ +export function formatUserCount(userCount: number): string { + const userCountRanges: { min: number; max: number; label: string }[] = [ + { min: 0, max: 1, label: "1" }, + { min: 2, max: 9, label: "1-10" }, + { min: 10, max: 49, label: "10+" }, + { min: 50, max: 99, label: "50+" }, + { min: 100, max: 299, label: "100+" }, + { min: 300, max: 499, label: "300+" }, + { min: 500, max: 999, label: "500+" }, + { min: 1_000, max: 2_999, label: "1k+" }, + { min: 3_000, max: 4_999, label: "3k+" }, + { min: 5_000, max: 9_999, label: "5k+" }, + { min: 10_000, max: 19_999, label: "10k+" }, + { min: 20_000, max: 29_999, label: "20k+" }, + { min: 30_000, max: 39_999, label: "30k+" }, + { min: 40_000, max: 49_999, label: "40k+" }, + { min: 50_000, max: 59_999, label: "50k+" }, + { min: 60_000, max: 69_999, label: "60k+" }, + { min: 70_000, max: 79_999, label: "70k+" }, + { min: 80_000, max: 89_999, label: "80k+" }, + { min: 90_000, max: 99_999, label: "90k+" }, + { min: 100_000, max: 109_999, label: "100k+" }, + { min: 110_000, max: 119_999, label: "110k+" }, + { min: 120_000, max: 129_999, label: "120k+" }, + { min: 130_000, max: 139_999, label: "130k+" }, + { min: 140_000, max: 149_999, label: "140k+" }, + { min: 150_000, max: 199_999, label: "150k+" }, + { min: 200_000, max: 299_999, label: "200k+" }, + { min: 300_000, max: 499_999, label: "300k+" }, + { min: 500_000, max: 749_999, label: "500k+" }, + { min: 750_000, max: 999_999, label: "750k+" }, + { min: 1_000_000, max: Infinity, label: "1M+" }, + ]; + + const range = userCountRanges.find(({ min, max }) => userCount >= min && userCount <= max); + return range?.label ?? ""; +} diff --git a/src/lib/utils/getGradioApi.ts b/src/lib/utils/getGradioApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..8dd8b07c8660bcaf5e9171fa8371f82a959e6ffd --- /dev/null +++ b/src/lib/utils/getGradioApi.ts @@ -0,0 +1,16 @@ +import { base } from "$app/paths"; +import type { Client } from "@gradio/client"; + +export type ApiReturnType = Awaited>; + +export async function getGradioApi(space: string) { + const api: ApiReturnType = await fetch(`${base}/api/spaces-config?space=${space}`).then( + async (res) => { + if (!res.ok) { + throw new Error(await res.text()); + } + return res.json(); + } + ); + return api; +} diff --git a/src/lib/utils/getHref.ts b/src/lib/utils/getHref.ts new file mode 100644 index 0000000000000000000000000000000000000000..af5a0a1262520b6cb12c79e81fb3b182ce2cc19d --- /dev/null +++ b/src/lib/utils/getHref.ts @@ -0,0 +1,41 @@ +export function getHref( + url: URL | string, + modifications: { + newKeys?: Record; + existingKeys?: { behaviour: "delete_except" | "delete"; keys: string[] }; + } +) { + const newUrl = new URL(url); + const { newKeys, existingKeys } = modifications; + + // exsiting keys logic + if (existingKeys) { + const { behaviour, keys } = existingKeys; + if (behaviour === "delete") { + for (const key of keys) { + newUrl.searchParams.delete(key); + } + } else { + // delete_except + const keysToPreserve = keys; + for (const key of [...newUrl.searchParams.keys()]) { + if (!keysToPreserve.includes(key)) { + newUrl.searchParams.delete(key); + } + } + } + } + + // new keys logic + if (newKeys) { + for (const [key, val] of Object.entries(newKeys)) { + if (val) { + newUrl.searchParams.set(key, val); + } else { + newUrl.searchParams.delete(key); + } + } + } + + return newUrl.toString(); +} diff --git a/src/lib/utils/getReturnFromGenerator.ts b/src/lib/utils/getReturnFromGenerator.ts new file mode 100644 index 0000000000000000000000000000000000000000..cfb3283cba59064f496f5496a6abab7488ff35f0 --- /dev/null +++ b/src/lib/utils/getReturnFromGenerator.ts @@ -0,0 +1,7 @@ +export async function getReturnFromGenerator(generator: AsyncGenerator): Promise { + let result: IteratorResult; + do { + result = await generator.next(); + } while (!result.done); // Keep calling `next()` until `done` is true + return result.value; // Return the final value +} diff --git a/src/lib/utils/getShareUrl.ts b/src/lib/utils/getShareUrl.ts new file mode 100644 index 0000000000000000000000000000000000000000..5278ab6fd6ef21b1c34468a0c9b81a164f78b52b --- /dev/null +++ b/src/lib/utils/getShareUrl.ts @@ -0,0 +1,8 @@ +import { base } from "$app/paths"; +import { env as envPublic } from "$env/dynamic/public"; + +export function getShareUrl(url: URL, shareId: string): string { + return `${ + envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}` + }/r/${shareId}`; +} diff --git a/src/lib/utils/getTokenizer.ts b/src/lib/utils/getTokenizer.ts new file mode 100644 index 0000000000000000000000000000000000000000..1c6be9b153d710f4c5173a5ae9e6d6e32656eba0 --- /dev/null +++ b/src/lib/utils/getTokenizer.ts @@ -0,0 +1,18 @@ +import type { Model } from "$lib/types/Model"; +import { AutoTokenizer, PreTrainedTokenizer } from "@huggingface/transformers"; + +export async function getTokenizer(_modelTokenizer: Exclude) { + if (typeof _modelTokenizer === "string") { + // return auto tokenizer + return await AutoTokenizer.from_pretrained(_modelTokenizer); + } else { + // construct & return pretrained tokenizer + const { tokenizerUrl, tokenizerConfigUrl } = _modelTokenizer satisfies { + tokenizerUrl: string; + tokenizerConfigUrl: string; + }; + const tokenizerJSON = await (await fetch(tokenizerUrl)).json(); + const tokenizerConfig = await (await fetch(tokenizerConfigUrl)).json(); + return new PreTrainedTokenizer(tokenizerJSON, tokenizerConfig); + } +} diff --git a/src/lib/utils/hashConv.ts b/src/lib/utils/hashConv.ts new file mode 100644 index 0000000000000000000000000000000000000000..de014324f6f21fbb67a61d098844027cfcdad0bf --- /dev/null +++ b/src/lib/utils/hashConv.ts @@ -0,0 +1,12 @@ +import type { Conversation } from "$lib/types/Conversation"; +import { sha256 } from "./sha256"; + +export async function hashConv(conv: Conversation) { + // messages contains the conversation message but only the immutable part + const messages = conv.messages.map((message) => { + return (({ from, id, content, webSearchId }) => ({ from, id, content, webSearchId }))(message); + }); + + const hash = await sha256(JSON.stringify(messages)); + return hash; +} diff --git a/src/lib/utils/isDesktop.ts b/src/lib/utils/isDesktop.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d76f7dca420071cd1385bf8e515efaf64cc7cff --- /dev/null +++ b/src/lib/utils/isDesktop.ts @@ -0,0 +1,7 @@ +// Approximate width from which we disable autofocus +const TABLET_VIEWPORT_WIDTH = 768; + +export function isDesktop(window: Window) { + const { innerWidth } = window; + return innerWidth > TABLET_VIEWPORT_WIDTH; +} diff --git a/src/lib/utils/isHuggingChat.ts b/src/lib/utils/isHuggingChat.ts new file mode 100644 index 0000000000000000000000000000000000000000..df1ad80039eb147a5427cd5ca1980e92b5c2c22a --- /dev/null +++ b/src/lib/utils/isHuggingChat.ts @@ -0,0 +1,3 @@ +import { env as envPublic } from "$env/dynamic/public"; + +export const isHuggingChat = envPublic.PUBLIC_APP_ASSETS === "huggingchat"; diff --git a/src/lib/utils/isUrl.ts b/src/lib/utils/isUrl.ts new file mode 100644 index 0000000000000000000000000000000000000000..d24c0eaa499614a7c5b208bd2c1684e18227bfec --- /dev/null +++ b/src/lib/utils/isUrl.ts @@ -0,0 +1,8 @@ +export function isURL(url: string) { + try { + new URL(url); + return true; + } catch (e) { + return false; + } +} diff --git a/src/lib/utils/marked.ts b/src/lib/utils/marked.ts new file mode 100644 index 0000000000000000000000000000000000000000..84fa7bc5081a4d3796757541c9121ae9cf3b3e05 --- /dev/null +++ b/src/lib/utils/marked.ts @@ -0,0 +1,224 @@ +import katex from "katex"; +import "katex/dist/contrib/mhchem.mjs"; +import { Marked } from "marked"; +import type { Tokens, TokenizerExtension, RendererExtension } from "marked"; +import type { WebSearchSource } from "$lib/types/WebSearch"; +import hljs from "highlight.js"; + +interface katexBlockToken extends Tokens.Generic { + type: "katexBlock"; + raw: string; + text: string; + displayMode: true; +} + +interface katexInlineToken extends Tokens.Generic { + type: "katexInline"; + raw: string; + text: string; + displayMode: false; +} + +export const katexBlockExtension: TokenizerExtension & RendererExtension = { + name: "katexBlock", + level: "block", + + start(src: string): number | undefined { + const match = src.match(/(\${2}|\\\[)/); + return match ? match.index : -1; + }, + + tokenizer(src: string): katexBlockToken | undefined { + // 1) $$ ... $$ + const rule1 = /^\${2}([\s\S]+?)\${2}/; + const match1 = rule1.exec(src); + if (match1) { + const token: katexBlockToken = { + type: "katexBlock", + raw: match1[0], + text: match1[1].trim(), + displayMode: true, + }; + return token; + } + + // 2) \[ ... \] + const rule2 = /^\\\[([\s\S]+?)\\\]/; + const match2 = rule2.exec(src); + if (match2) { + const token: katexBlockToken = { + type: "katexBlock", + raw: match2[0], + text: match2[1].trim(), + displayMode: true, + }; + return token; + } + + return undefined; + }, + + renderer(token) { + if (token.type === "katexBlock") { + return katex.renderToString(token.text, { + throwOnError: false, + displayMode: token.displayMode, + }); + } + return undefined; + }, +}; + +const katexInlineExtension: TokenizerExtension & RendererExtension = { + name: "katexInline", + level: "inline", + + start(src: string): number | undefined { + const match = src.match(/(\$|\\\()/); + return match ? match.index : -1; + }, + + tokenizer(src: string): katexInlineToken | undefined { + // 1) $...$ + const rule1 = /^\$([^$]+?)\$/; + const match1 = rule1.exec(src); + if (match1) { + const token: katexInlineToken = { + type: "katexInline", + raw: match1[0], + text: match1[1].trim(), + displayMode: false, + }; + return token; + } + + // 2) \(...\) + const rule2 = /^\\\(([\s\S]+?)\\\)/; + const match2 = rule2.exec(src); + if (match2) { + const token: katexInlineToken = { + type: "katexInline", + raw: match2[0], + text: match2[1].trim(), + displayMode: false, + }; + return token; + } + + return undefined; + }, + + renderer(token) { + if (token.type === "katexInline") { + return katex.renderToString(token.text, { + throwOnError: false, + displayMode: token.displayMode, + }); + } + return undefined; + }, +}; + +function escapeHTML(content: string) { + return content.replace( + /[<>&"']/g, + (x) => + ({ + "<": "<", + ">": ">", + "&": "&", + "'": "'", + '"': """, + })[x] || x + ); +} + +function addInlineCitations(md: string, webSearchSources: WebSearchSource[] = []): string { + const linkStyle = + "color: rgb(59, 130, 246); text-decoration: none; hover:text-decoration: underline;"; + return md.replace(/\[(\d+)\]/g, (match: string) => { + const indices: number[] = (match.match(/\d+/g) || []).map(Number); + const links: string = indices + .map((index: number) => { + if (index === 0) return false; + const source = webSearchSources[index - 1]; + if (source) { + return `${index}`; + } + return ""; + }) + .filter(Boolean) + .join(", "); + return links ? ` ${links}` : match; + }); +} + +function createMarkedInstance(sources: WebSearchSource[]): Marked { + return new Marked({ + hooks: { + postprocess: (html) => addInlineCitations(html, sources), + }, + extensions: [katexBlockExtension, katexInlineExtension], + renderer: { + link: (href, title, text) => + `${text}`, + html: (html) => escapeHTML(html), + }, + gfm: true, + breaks: true, + }); +} +type CodeToken = { + type: "code"; + lang: string; + code: string; + rawCode: string; +}; + +type TextToken = { + type: "text"; + html: string | Promise; +}; + +export async function processTokens(content: string, sources: WebSearchSource[]): Promise { + const marked = createMarkedInstance(sources); + const tokens = marked.lexer(content); + + const processedTokens = await Promise.all( + tokens.map(async (token) => { + if (token.type === "code") { + return { + type: "code" as const, + lang: token.lang, + code: hljs.highlightAuto(token.text, hljs.getLanguage(token.lang)?.aliases).value, + rawCode: token.text, + }; + } else { + return { + type: "text" as const, + html: marked.parse(token.raw), + }; + } + }) + ); + + return processedTokens; +} + +export function processTokensSync(content: string, sources: WebSearchSource[]): Token[] { + const marked = createMarkedInstance(sources); + const tokens = marked.lexer(content); + return tokens.map((token) => { + if (token.type === "code") { + return { + type: "code" as const, + lang: token.lang, + code: hljs.highlightAuto(token.text, hljs.getLanguage(token.lang)?.aliases).value, + rawCode: token.text, + }; + } + return { type: "text" as const, html: marked.parse(token.raw) }; + }); +} + +export type Token = CodeToken | TextToken; diff --git a/src/lib/utils/mergeAsyncGenerators.ts b/src/lib/utils/mergeAsyncGenerators.ts new file mode 100644 index 0000000000000000000000000000000000000000..08544298c69d75f638daaf30c14084d106dd6009 --- /dev/null +++ b/src/lib/utils/mergeAsyncGenerators.ts @@ -0,0 +1,38 @@ +type Gen = AsyncGenerator; + +type GenPromiseMap = Map< + Gen, + Promise<{ gen: Gen } & IteratorResult> +>; + +/** Merges multiple async generators into a single async generator that yields values from all of them in parallel. */ +export async function* mergeAsyncGenerators( + generators: Gen[] +): Gen { + const promises: GenPromiseMap = new Map(); + const results: Map, TReturn> = new Map(); + + for (const gen of generators) { + promises.set( + gen, + gen.next().then((result) => ({ gen, ...result })) + ); + } + + while (promises.size) { + const { gen, value, done } = await Promise.race(promises.values()); + if (done) { + results.set(gen, value as TReturn); + promises.delete(gen); + } else { + promises.set( + gen, + gen.next().then((result) => ({ gen, ...result })) + ); + yield value as T; + } + } + + const orderedResults = generators.map((gen) => results.get(gen) as TReturn); + return orderedResults; +} diff --git a/src/lib/utils/messageUpdates.ts b/src/lib/utils/messageUpdates.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b9baaec9b54a0a42f4d20db61ea858571977e3d --- /dev/null +++ b/src/lib/utils/messageUpdates.ts @@ -0,0 +1,273 @@ +import type { MessageFile } from "$lib/types/Message"; +import { + type MessageUpdate, + type MessageStreamUpdate, + type MessageToolCallUpdate, + MessageToolUpdateType, + MessageUpdateType, + type MessageToolUpdate, + type MessageWebSearchUpdate, + type MessageWebSearchGeneralUpdate, + type MessageWebSearchSourcesUpdate, + type MessageWebSearchErrorUpdate, + MessageWebSearchUpdateType, + type MessageToolErrorUpdate, + type MessageToolResultUpdate, +} from "$lib/types/MessageUpdate"; +import { env as envPublic } from "$env/dynamic/public"; + +export const isMessageWebSearchUpdate = (update: MessageUpdate): update is MessageWebSearchUpdate => + update.type === MessageUpdateType.WebSearch; +export const isMessageWebSearchGeneralUpdate = ( + update: MessageUpdate +): update is MessageWebSearchGeneralUpdate => + isMessageWebSearchUpdate(update) && update.subtype === MessageWebSearchUpdateType.Update; +export const isMessageWebSearchSourcesUpdate = ( + update: MessageUpdate +): update is MessageWebSearchSourcesUpdate => + isMessageWebSearchUpdate(update) && update.subtype === MessageWebSearchUpdateType.Sources; +export const isMessageWebSearchErrorUpdate = ( + update: MessageUpdate +): update is MessageWebSearchErrorUpdate => + isMessageWebSearchUpdate(update) && update.subtype === MessageWebSearchUpdateType.Error; + +export const isMessageToolUpdate = (update: MessageUpdate): update is MessageToolUpdate => + update.type === MessageUpdateType.Tool; +export const isMessageToolCallUpdate = (update: MessageUpdate): update is MessageToolCallUpdate => + isMessageToolUpdate(update) && update.subtype === MessageToolUpdateType.Call; +export const isMessageToolResultUpdate = ( + update: MessageUpdate +): update is MessageToolResultUpdate => + isMessageToolUpdate(update) && update.subtype === MessageToolUpdateType.Result; +export const isMessageToolErrorUpdate = (update: MessageUpdate): update is MessageToolErrorUpdate => + isMessageToolUpdate(update) && update.subtype === MessageToolUpdateType.Error; + +type MessageUpdateRequestOptions = { + base: string; + inputs?: string; + messageId?: string; + isRetry: boolean; + isContinue: boolean; + webSearch: boolean; + tools?: Array; + files?: MessageFile[]; +}; +export async function fetchMessageUpdates( + conversationId: string, + opts: MessageUpdateRequestOptions, + abortSignal: AbortSignal +): Promise> { + const abortController = new AbortController(); + abortSignal.addEventListener("abort", () => abortController.abort()); + + const form = new FormData(); + + const optsJSON = JSON.stringify({ + inputs: opts.inputs, + id: opts.messageId, + is_retry: opts.isRetry, + is_continue: opts.isContinue, + web_search: opts.webSearch, + tools: opts.tools, + }); + + opts.files?.forEach((file) => { + const name = file.type + ";" + file.name; + + form.append("files", new File([file.value], name, { type: file.mime })); + }); + + form.append("data", optsJSON); + + const response = await fetch(`${opts.base}/conversation/${conversationId}`, { + method: "POST", + body: form, + signal: abortController.signal, + }); + + if (!response.ok) { + const errorMessage = await response + .json() + .then((obj) => obj.message) + .catch(() => `Request failed with status code ${response.status}: ${response.statusText}`); + throw Error(errorMessage); + } + if (!response.body) { + throw Error("Body not defined"); + } + + if (!(envPublic.PUBLIC_SMOOTH_UPDATES === "true")) { + return endpointStreamToIterator(response, abortController); + } + + return smoothAsyncIterator( + streamMessageUpdatesToFullWords(endpointStreamToIterator(response, abortController)) + ); +} + +async function* endpointStreamToIterator( + response: Response, + abortController: AbortController +): AsyncGenerator { + const reader = response.body?.pipeThrough(new TextDecoderStream()).getReader(); + if (!reader) throw Error("Response for endpoint had no body"); + + // Handle any cases where we must abort + reader.closed.then(() => abortController.abort()); + + // Handle logic for aborting + abortController.signal.addEventListener("abort", () => reader.cancel()); + + // ex) If the last response is => {"type": "stream", "token": + // It should be => {"type": "stream", "token": "Hello"} = prev_input_chunk + "Hello"} + let prevChunk = ""; + while (!abortController.signal.aborted) { + const { done, value } = await reader.read(); + if (done) { + abortController.abort(); + break; + } + if (!value) continue; + + const { messageUpdates, remainingText } = parseMessageUpdates(prevChunk + value); + prevChunk = remainingText; + for (const messageUpdate of messageUpdates) yield messageUpdate; + } +} + +function parseMessageUpdates(value: string): { + messageUpdates: MessageUpdate[]; + remainingText: string; +} { + const inputs = value.split("\n"); + const messageUpdates: MessageUpdate[] = []; + for (const input of inputs) { + try { + messageUpdates.push(JSON.parse(input) as MessageUpdate); + } catch (error) { + // in case of parsing error, we return what we were able to parse + if (error instanceof SyntaxError) { + return { + messageUpdates, + remainingText: inputs.at(-1) ?? "", + }; + } + } + } + return { messageUpdates, remainingText: "" }; +} + +/** + * Emits all the message updates immediately that aren't "stream" type + * Emits a concatenated "stream" type message update once it detects a full word + * Example: "what" " don" "'t" => "what" " don't" + * Only supports latin languages, ignores others + */ +async function* streamMessageUpdatesToFullWords( + iterator: AsyncGenerator +): AsyncGenerator { + let bufferedStreamUpdates: MessageStreamUpdate[] = []; + + const endAlphanumeric = /[a-zA-Z0-9À-ž'`]+$/; + const beginnningAlphanumeric = /^[a-zA-Z0-9À-ž'`]+/; + + for await (const messageUpdate of iterator) { + if (messageUpdate.type !== "stream") { + yield messageUpdate; + continue; + } + bufferedStreamUpdates.push(messageUpdate); + + let lastIndexEmitted = 0; + for (let i = 1; i < bufferedStreamUpdates.length; i++) { + const prevEndsAlphanumeric = endAlphanumeric.test(bufferedStreamUpdates[i - 1].token); + const currBeginsAlphanumeric = beginnningAlphanumeric.test(bufferedStreamUpdates[i].token); + const shouldCombine = prevEndsAlphanumeric && currBeginsAlphanumeric; + const combinedTooMany = i - lastIndexEmitted >= 5; + if (shouldCombine && !combinedTooMany) continue; + + // Combine tokens together and emit + yield { + type: MessageUpdateType.Stream, + token: bufferedStreamUpdates + .slice(lastIndexEmitted, i) + .map((_) => _.token) + .join(""), + }; + lastIndexEmitted = i; + } + bufferedStreamUpdates = bufferedStreamUpdates.slice(lastIndexEmitted); + } + for (const messageUpdate of bufferedStreamUpdates) yield messageUpdate; +} + +/** + * Attempts to smooth out the time between values emitted by an async iterator + * by waiting for the average time between values to emit the next value + */ +async function* smoothAsyncIterator(iterator: AsyncGenerator): AsyncGenerator { + const eventTarget = new EventTarget(); + let done = false; + const valuesBuffer: T[] = []; + const valueTimesMS: number[] = []; + + const next = async () => { + const obj = await iterator.next(); + if (obj.done) { + done = true; + } else { + valuesBuffer.push(obj.value); + valueTimesMS.push(performance.now()); + next(); + } + eventTarget.dispatchEvent(new Event("next")); + }; + next(); + + let timeOfLastEmitMS = performance.now(); + while (!done || valuesBuffer.length > 0) { + // Only consider the last X times between tokens + const sampledTimesMS = valueTimesMS.slice(-30); + + // Get the total time spent in abnormal periods + const anomalyThresholdMS = 2000; + const anomalyDurationMS = sampledTimesMS + .map((time, i, times) => time - times[i - 1]) + .slice(1) + .filter((time) => time > anomalyThresholdMS) + .reduce((a, b) => a + b, 0); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const totalTimeMSBetweenValues = sampledTimesMS.at(-1)! - sampledTimesMS[0]; + const timeMSBetweenValues = totalTimeMSBetweenValues - anomalyDurationMS; + + const averageTimeMSBetweenValues = Math.min( + 200, + timeMSBetweenValues / (sampledTimesMS.length - 1) + ); + const timeSinceLastEmitMS = performance.now() - timeOfLastEmitMS; + + // Emit after waiting duration or cancel if "next" event is emitted + const gotNext = await Promise.race([ + sleep(Math.max(5, averageTimeMSBetweenValues - timeSinceLastEmitMS)), + waitForEvent(eventTarget, "next"), + ]); + + // Go to next iteration so we can re-calculate when to emit + if (gotNext) continue; + + // Nothing in buffer to emit + if (valuesBuffer.length === 0) continue; + + // Emit + timeOfLastEmitMS = performance.now(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + yield valuesBuffer.shift()!; + } +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const waitForEvent = (eventTarget: EventTarget, eventName: string) => + new Promise((resolve) => + eventTarget.addEventListener(eventName, () => resolve(true), { once: true }) + ); diff --git a/src/lib/utils/models.ts b/src/lib/utils/models.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d3f6949d37073ae552b2e9e7677d9aba427c0eb --- /dev/null +++ b/src/lib/utils/models.ts @@ -0,0 +1,4 @@ +import type { Model } from "$lib/types/Model"; + +export const findCurrentModel = (models: Model[], id?: string): Model => + models.find((m) => m.id === id) ?? models[0]; diff --git a/src/lib/utils/parseStringToList.ts b/src/lib/utils/parseStringToList.ts new file mode 100644 index 0000000000000000000000000000000000000000..a082057c665187d30fe0a49ceb84b9e7552ffc1e --- /dev/null +++ b/src/lib/utils/parseStringToList.ts @@ -0,0 +1,10 @@ +export function parseStringToList(links: unknown): string[] { + if (typeof links !== "string") { + throw new Error("Expected a string"); + } + + return links + .split(",") + .map((link) => link.trim()) + .filter((link) => link.length > 0); +} diff --git a/src/lib/utils/randomUuid.ts b/src/lib/utils/randomUuid.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d536365c57659305ad28d6fc06b89d77ab337ab --- /dev/null +++ b/src/lib/utils/randomUuid.ts @@ -0,0 +1,14 @@ +type UUID = ReturnType; + +export function randomUUID(): UUID { + // Only on old safari / ios + if (!("randomUUID" in crypto)) { + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => + ( + Number(c) ^ + (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (Number(c) / 4))) + ).toString(16) + ) as UUID; + } + return crypto.randomUUID(); +} diff --git a/src/lib/utils/screenshot.ts b/src/lib/utils/screenshot.ts new file mode 100644 index 0000000000000000000000000000000000000000..ebfee90a82deb2f6148a9ae543fd2677b776b9c8 --- /dev/null +++ b/src/lib/utils/screenshot.ts @@ -0,0 +1,43 @@ +export async function captureScreen(): Promise { + let stream: MediaStream | undefined; + try { + // This will show the native browser dialog for screen capture + stream = await navigator.mediaDevices.getDisplayMedia({ + video: true, + audio: false, + }); + + // Create a canvas element to capture the screenshot + const canvas = document.createElement("canvas"); + const video = document.createElement("video"); + + // Wait for the video to load metadata + await new Promise((resolve) => { + video.onloadedmetadata = () => { + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + video.play(); + resolve(null); + }; + if (stream) { + video.srcObject = stream; + } else { + throw Error("No stream available"); + } + }); + + // Draw the video frame to canvas + const context = canvas.getContext("2d"); + context?.drawImage(video, 0, 0, canvas.width, canvas.height); + // Convert to base64 + return canvas.toDataURL("image/png"); + } catch (error) { + console.error("Error capturing screenshot:", error); + throw error; + } finally { + // Stop all tracks + if (stream) { + stream.getTracks().forEach((track) => track.stop()); + } + } +} diff --git a/src/lib/utils/searchTokens.ts b/src/lib/utils/searchTokens.ts new file mode 100644 index 0000000000000000000000000000000000000000..012df9b644736de94c2c7cfa12f70f5d5eb4b253 --- /dev/null +++ b/src/lib/utils/searchTokens.ts @@ -0,0 +1,33 @@ +const PUNCTUATION_REGEX = /\p{P}/gu; + +function removeDiacritics(s: string, form: "NFD" | "NFKD" = "NFD"): string { + return s.normalize(form).replace(/[\u0300-\u036f]/g, ""); +} + +export function generateSearchTokens(value: string): string[] { + const fullTitleToken = removeDiacritics(value) + .replace(PUNCTUATION_REGEX, "") + .replaceAll(/\s+/g, "") + .toLowerCase(); + return [ + ...new Set([ + ...removeDiacritics(value) + .split(/\s+/) + .map((word) => word.replace(PUNCTUATION_REGEX, "").toLowerCase()) + .filter((word) => word.length), + ...(fullTitleToken.length ? [fullTitleToken] : []), + ]), + ]; +} + +function escapeForRegExp(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string +} + +export function generateQueryTokens(query: string): RegExp[] { + return removeDiacritics(query) + .split(/\s+/) + .map((word) => word.replace(PUNCTUATION_REGEX, "").toLowerCase()) + .filter((word) => word.length) + .map((token) => new RegExp(`^${escapeForRegExp(token)}`)); +} diff --git a/src/lib/utils/sha256.ts b/src/lib/utils/sha256.ts new file mode 100644 index 0000000000000000000000000000000000000000..43059b518fc5a4da6ed08ab36aeb6c289007f6aa --- /dev/null +++ b/src/lib/utils/sha256.ts @@ -0,0 +1,7 @@ +export async function sha256(input: string): Promise { + const utf8 = new TextEncoder().encode(input); + const hashBuffer = await crypto.subtle.digest("SHA-256", utf8); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((bytes) => bytes.toString(16).padStart(2, "0")).join(""); + return hashHex; +} diff --git a/src/lib/utils/share.ts b/src/lib/utils/share.ts new file mode 100644 index 0000000000000000000000000000000000000000..25352732f646a8bdd44ae667d8e90a67959accdc --- /dev/null +++ b/src/lib/utils/share.ts @@ -0,0 +1,28 @@ +import { browser } from "$app/environment"; +import { isDesktop } from "./isDesktop"; + +export async function share(url: string, title: string, appendLeafId: boolean = false) { + if (!browser) return; + + // Retrieve the leafId from localStorage + const leafId = localStorage.getItem("leafId"); + + if (appendLeafId && leafId) { + // Use URL and URLSearchParams to add the leafId parameter + const shareUrl = new URL(url); + shareUrl.searchParams.append("leafId", leafId); + url = shareUrl.toString(); + } + + if (navigator.share && !isDesktop(window)) { + navigator.share({ url, title }); + } else { + // this is really ugly + // but on chrome the clipboard write doesn't work if the window isn't focused + // and after we use confirm() to ask the user if they want to share, the window is no longer focused + // for a few ms until the confirm dialog closes. tried await tick(), tried window.focus(), didnt work + // bug doesnt occur in firefox, if you can find a better fix for it please do + await new Promise((resolve) => setTimeout(resolve, 250)); + await navigator.clipboard.writeText(url); + } +} diff --git a/src/lib/utils/stringifyError.ts b/src/lib/utils/stringifyError.ts new file mode 100644 index 0000000000000000000000000000000000000000..a182d0974e0f8570d392d94fb957fcf49bc17692 --- /dev/null +++ b/src/lib/utils/stringifyError.ts @@ -0,0 +1,12 @@ +/** Takes an unknown error and attempts to convert it to a string */ +export function stringifyError(error: unknown): string { + if (error instanceof Error) return error.message; + if (typeof error === "string") return error; + if (typeof error === "object" && error !== null) { + // try a few common properties + if ("message" in error && typeof error.message === "string") return error.message; + if ("body" in error && typeof error.body === "string") return error.body; + if ("name" in error && typeof error.name === "string") return error.name; + } + return "Unknown error"; +} diff --git a/src/lib/utils/sum.ts b/src/lib/utils/sum.ts new file mode 100644 index 0000000000000000000000000000000000000000..289b70584ef9f7795b1f4b1bf0151237dc2c55ff --- /dev/null +++ b/src/lib/utils/sum.ts @@ -0,0 +1,3 @@ +export function sum(nums: number[]): number { + return nums.reduce((a, b) => a + b, 0); +} diff --git a/src/lib/utils/template.spec.ts b/src/lib/utils/template.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f3462b6ef006d42e4c17b40c995f92422c0762d --- /dev/null +++ b/src/lib/utils/template.spec.ts @@ -0,0 +1,59 @@ +import { describe, test, expect } from "vitest"; +import { compileTemplate } from "./template"; + +// Test data for simple templates +const modelData = { + preprompt: "Hello", +}; + +const simpleTemplate = "Test: {{preprompt}} and {{foo}}"; + +// Additional realistic test data for Llama 70B templates +const messages = [ + { from: "user", content: "Hello there" }, + { from: "assistant", content: "Hi, how can I help?" }, +]; + +// Handlebars Llama 70B Template +const llama70bTemplateHB = `{{#if preprompt}}Source: system\n\n{{preprompt}}{{/if}}{{#each messages}}{{#ifUser}}Source: user\n\n{{content}}{{/ifUser}}{{#ifAssistant}}Source: assistant\n\n{{content}}{{/ifAssistant}}{{/each}}Source: assistant\nDestination: user\n\n`; + +// Expected output for Handlebars Llama 70B Template +const expectedHB = + "Source: system\n\nSystem MessageSource: user\n\nHello thereSource: assistant\n\nHi, how can I help?Source: assistant\nDestination: user\n\n"; + +// Jinja Llama 70B Template +const llama70bTemplateJinja = `{% if preprompt %}Source: system\n\n{{ preprompt }}{% endif %}{% for message in messages %}{% if message.from == 'user' %}Source: user\n\n{{ message.content }}{% elif message.from == 'assistant' %}Source: assistant\n\n{{ message.content }}{% endif %}{% endfor %}Source: assistant\nDestination: user\n\n`; + +// Expected output for Jinja Llama 70B Template +const expectedJinja = + "Source: system\n\nSystem MessageSource: user\n\nHello thereSource: assistant\n\nHi, how can I help?Source: assistant\nDestination: user\n\n"; + +describe("Template Engine Rendering", () => { + test("should render using Handlebars fallback when no templateEngine is specified", () => { + const render = compileTemplate(simpleTemplate, modelData); + const result = render({ foo: "World" }); + expect(result).toBe("Test: Hello and World"); + }); + + test('should render using Jinja when templateEngine is set to "jinja"', () => { + const render = compileTemplate(simpleTemplate, modelData); + const result = render({ foo: "World" }); + expect(result).toBe("Test: Hello and World"); + }); + + // Realistic Llama 70B template tests + test("should render realistic Llama 70B template using Handlebars", () => { + const render = compileTemplate(llama70bTemplateHB, { preprompt: "System Message" }); + const result = render({ messages }); + expect(result).toBe(expectedHB); + }); + + test("should render realistic Llama 70B template using Jinja", () => { + const render = compileTemplate(llama70bTemplateJinja, { + preprompt: "System Message", + }); + const result = render({ messages }); + // Trim both outputs to account for whitespace differences in Jinja engine + expect(result.trim()).toBe(expectedJinja.trim()); + }); +}); diff --git a/src/lib/utils/template.ts b/src/lib/utils/template.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e4d1a8005f486aa58aa92448df4332fa924fadc --- /dev/null +++ b/src/lib/utils/template.ts @@ -0,0 +1,50 @@ +import type { Message } from "$lib/types/Message"; +import Handlebars from "handlebars"; +import { Template } from "@huggingface/jinja"; + +// Register Handlebars helpers +Handlebars.registerHelper("ifUser", function (this: Pick, options) { + if (this.from == "user") return options.fn(this); +}); + +Handlebars.registerHelper( + "ifAssistant", + function (this: Pick, options) { + if (this.from == "assistant") return options.fn(this); + } +); + +// Updated compileTemplate to try Jinja and fallback to Handlebars if Jinja fails +export function compileTemplate( + input: string, + model: { preprompt: string; templateEngine?: string } +) { + let jinjaTemplate: Template | undefined; + try { + // Try to compile with Jinja + jinjaTemplate = new Template(input); + } catch (e) { + // Could not compile with Jinja + jinjaTemplate = undefined; + } + + const hbTemplate = Handlebars.compile(input, { + knownHelpers: { ifUser: true, ifAssistant: true }, + knownHelpersOnly: true, + noEscape: true, + strict: true, + preventIndent: true, + }); + + return function render(inputs: T) { + if (jinjaTemplate) { + try { + return jinjaTemplate.render({ ...model, ...inputs }); + } catch (e) { + // Fallback to Handlebars if Jinja rendering fails + return hbTemplate({ ...model, ...inputs }); + } + } + return hbTemplate({ ...model, ...inputs }); + }; +} diff --git a/src/lib/utils/timeout.ts b/src/lib/utils/timeout.ts new file mode 100644 index 0000000000000000000000000000000000000000..355edd12e5f2257a7b30309b1fca8fc8cb592c8d --- /dev/null +++ b/src/lib/utils/timeout.ts @@ -0,0 +1,9 @@ +export const timeout = (prom: Promise, time: number): Promise => { + let timer: NodeJS.Timeout; + return Promise.race([ + prom, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`Timeout after ${time / 1000} seconds`)), time); + }), + ]).finally(() => clearTimeout(timer)); +}; diff --git a/src/lib/utils/toolIds.ts b/src/lib/utils/toolIds.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b2f74851d785447e51c992cd2559bac0cae039a --- /dev/null +++ b/src/lib/utils/toolIds.ts @@ -0,0 +1,4 @@ +export const webSearchToolId = "00000000000000000000000a"; +export const fetchUrlToolId = "00000000000000000000000b"; +export const imageGenToolId = "000000000000000000000001"; +export const documentParserToolId = "000000000000000000000002"; diff --git a/src/lib/utils/tools.ts b/src/lib/utils/tools.ts new file mode 100644 index 0000000000000000000000000000000000000000..52b3077c092b2a1500df04a3a36b4c859c09dd83 --- /dev/null +++ b/src/lib/utils/tools.ts @@ -0,0 +1,25 @@ +import type { Tool } from "$lib/types/Tool"; + +/** + * Checks if a tool's name equals a value. Replaces all hyphens with underscores before comparison + * since some models return underscores even when hyphens are used in the request. + **/ +export function toolHasName(name: string, tool: Pick): boolean { + return tool.name.replaceAll("-", "_") === name.replaceAll("-", "_"); +} + +export const colors = ["purple", "blue", "green", "yellow", "red"] as const; + +export const icons = [ + "wikis", + "tools", + "camera", + "code", + "email", + "cloud", + "terminal", + "game", + "chat", + "speaker", + "video", +] as const; diff --git a/src/lib/utils/tree/addChildren.spec.ts b/src/lib/utils/tree/addChildren.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c7861f2cbeb6904c2d784ae298a1fd9992a897c --- /dev/null +++ b/src/lib/utils/tree/addChildren.spec.ts @@ -0,0 +1,102 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { describe, expect, it } from "vitest"; + +import { insertLegacyConversation, insertSideBranchesConversation } from "./treeHelpers.spec"; +import { addChildren } from "./addChildren"; +import type { Message } from "$lib/types/Message"; + +const newMessage: Omit = { + content: "new message", + from: "user", +}; + +Object.freeze(newMessage); + +describe("addChildren", async () => { + it("should let you append on legacy conversations", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const convLength = conv.messages.length; + + addChildren(conv, newMessage, conv.messages[conv.messages.length - 1].id); + expect(conv.messages.length).toEqual(convLength + 1); + }); + it("should not let you create branches on legacy conversations", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + expect(() => addChildren(conv, newMessage, conv.messages[0].id)).toThrow(); + }); + it("should not let you create a message that already exists", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const messageThatAlreadyExists: Message = { + id: conv.messages[0].id, + content: "new message", + from: "user", + }; + + expect(() => addChildren(conv, messageThatAlreadyExists, conv.messages[0].id)).toThrow(); + }); + it("should let you create branches on conversations with subtrees", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const nChildren = conv.messages[0].children?.length; + if (!nChildren) throw new Error("No children found"); + addChildren(conv, newMessage, conv.messages[0].id); + expect(conv.messages[0].children?.length).toEqual(nChildren + 1); + }); + + it("should let you create a new leaf", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const parentId = conv.messages[conv.messages.length - 1].id; + const nChildren = conv.messages[conv.messages.length - 1].children?.length; + + if (nChildren === undefined) throw new Error("No children found"); + expect(nChildren).toEqual(0); + + addChildren(conv, newMessage, parentId); + expect(conv.messages[conv.messages.length - 2].children?.length).toEqual(nChildren + 1); + }); + + it("should let you append to an empty conversation without specifying a parentId", async () => { + const conv = { + _id: new ObjectId(), + rootMessageId: undefined, + messages: [] as Message[], + }; + + addChildren(conv, newMessage); + expect(conv.messages.length).toEqual(1); + expect(conv.rootMessageId).toEqual(conv.messages[0].id); + }); + + it("should throw if you don't specify a parentId in a conversation with messages", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + expect(() => addChildren(conv, newMessage)).toThrow(); + }); + + it("should return the id of the new message", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + expect(addChildren(conv, newMessage, conv.messages[conv.messages.length - 1].id)).toEqual( + conv.messages[conv.messages.length - 1].id + ); + }); +}); diff --git a/src/lib/utils/tree/addChildren.ts b/src/lib/utils/tree/addChildren.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8149a1f62d810f76ab6cedb5c1fc0221b402f77 --- /dev/null +++ b/src/lib/utils/tree/addChildren.ts @@ -0,0 +1,53 @@ +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import { v4 } from "uuid"; + +export function addChildren( + conv: Pick, + message: Omit, + parentId?: Message["id"] +): Message["id"] { + // if this is the first message we just push it + if (conv.messages.length === 0) { + const messageId = v4(); + conv.rootMessageId = messageId; + conv.messages.push({ + ...message, + ancestors: [], + id: messageId, + }); + return messageId; + } + + if (!parentId) { + throw new Error("You need to specify a parentId if this is not the first message"); + } + + const messageId = v4(); + if (!conv.rootMessageId) { + // if there is no parentId we just push the message + if (!!parentId && parentId !== conv.messages[conv.messages.length - 1].id) { + throw new Error("This is a legacy conversation, you can only append to the last message"); + } + conv.messages.push({ ...message, id: messageId }); + return messageId; + } + + const ancestors = [...(conv.messages.find((m) => m.id === parentId)?.ancestors ?? []), parentId]; + conv.messages.push({ + ...message, + ancestors, + id: messageId, + children: [], + }); + + const parent = conv.messages.find((m) => m.id === parentId); + + if (parent) { + if (parent.children) { + parent.children.push(messageId); + } else parent.children = [messageId]; + } + + return messageId; +} diff --git a/src/lib/utils/tree/addSibling.spec.ts b/src/lib/utils/tree/addSibling.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac22836653b8564779a0eb5783a71545232573b8 --- /dev/null +++ b/src/lib/utils/tree/addSibling.spec.ts @@ -0,0 +1,80 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { describe, expect, it } from "vitest"; + +import { insertLegacyConversation, insertSideBranchesConversation } from "./treeHelpers.spec"; +import type { Message } from "$lib/types/Message"; +import { addSibling } from "./addSibling"; + +const newMessage: Omit = { + content: "new message", + from: "user", +}; + +Object.freeze(newMessage); + +describe("addSibling", async () => { + it("should fail on empty conversations", () => { + const conv = { + _id: new ObjectId(), + rootMessageId: undefined, + messages: [], + }; + + expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow( + "Cannot add a sibling to an empty conversation" + ); + }); + + it("should fail on legacy conversations", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + expect(() => addSibling(conv, newMessage, conv.messages[0].id)).toThrow( + "Cannot add a sibling to a legacy conversation" + ); + }); + + it("should fail if the sibling message doesn't exist", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow( + "The sibling message doesn't exist" + ); + }); + + // TODO: This behaviour should be fixed, we do not need to fail on the root message. + it("should fail if the sibling message is the root message", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + if (!conv.rootMessageId) throw new Error("Root message not found"); + + expect(() => addSibling(conv, newMessage, conv.rootMessageId as Message["id"])).toThrow( + "The sibling message is the root message, therefore we can't add a sibling" + ); + }); + + it("should add a sibling to a message", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + // add sibling and check children count for parnets + + const nChildren = conv.messages[1].children?.length; + const siblingId = addSibling(conv, newMessage, conv.messages[2].id); + const nChildrenNew = conv.messages[1].children?.length; + + if (!nChildren) throw new Error("No children found"); + + expect(nChildrenNew).toBe(nChildren + 1); + + // make sure siblings have the same ancestors + const sibling = conv.messages.find((m) => m.id === siblingId); + expect(sibling?.ancestors).toEqual(conv.messages[2].ancestors); + }); +}); diff --git a/src/lib/utils/tree/addSibling.ts b/src/lib/utils/tree/addSibling.ts new file mode 100644 index 0000000000000000000000000000000000000000..8371ffcf0f9ab7f2c75c592bc851f9556510af53 --- /dev/null +++ b/src/lib/utils/tree/addSibling.ts @@ -0,0 +1,46 @@ +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import { v4 } from "uuid"; + +export function addSibling( + conv: Pick, + message: Omit, + siblingId: Message["id"] +): Message["id"] { + if (conv.messages.length === 0) { + throw new Error("Cannot add a sibling to an empty conversation"); + } + if (!conv.rootMessageId) { + throw new Error("Cannot add a sibling to a legacy conversation"); + } + + const sibling = conv.messages.find((m) => m.id === siblingId); + + if (!sibling) { + throw new Error("The sibling message doesn't exist"); + } + + if (!sibling.ancestors || sibling.ancestors?.length === 0) { + throw new Error("The sibling message is the root message, therefore we can't add a sibling"); + } + + const messageId = v4(); + + conv.messages.push({ + ...message, + id: messageId, + ancestors: sibling.ancestors, + children: [], + }); + + const nearestAncestorId = sibling.ancestors[sibling.ancestors.length - 1]; + const nearestAncestor = conv.messages.find((m) => m.id === nearestAncestorId); + + if (nearestAncestor) { + if (nearestAncestor.children) { + nearestAncestor.children.push(messageId); + } else nearestAncestor.children = [messageId]; + } + + return messageId; +} diff --git a/src/lib/utils/tree/buildSubtree.spec.ts b/src/lib/utils/tree/buildSubtree.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..936fb8a2023417af71dca979154192ff9eb0ce73 --- /dev/null +++ b/src/lib/utils/tree/buildSubtree.spec.ts @@ -0,0 +1,110 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { describe, expect, it } from "vitest"; + +import { + insertLegacyConversation, + insertLinearBranchConversation, + insertSideBranchesConversation, +} from "./treeHelpers.spec"; +import { buildSubtree } from "./buildSubtree"; + +describe("buildSubtree", () => { + it("a subtree in a legacy conversation should be just a slice", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + // check middle + const id = conv.messages[2].id; + const subtree = buildSubtree(conv, id); + expect(subtree).toEqual(conv.messages.slice(0, 3)); + + // check zero + const id2 = conv.messages[0].id; + const subtree2 = buildSubtree(conv, id2); + expect(subtree2).toEqual(conv.messages.slice(0, 1)); + + //check full length + const id3 = conv.messages[conv.messages.length - 1].id; + const subtree3 = buildSubtree(conv, id3); + expect(subtree3).toEqual(conv.messages); + }); + + it("a subtree in a linear branch conversation should be the ancestors and the message", async () => { + const convId = await insertLinearBranchConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + // check middle + const id = conv.messages[1].id; + const subtree = buildSubtree(conv, id); + expect(subtree).toEqual([conv.messages[0], conv.messages[1]]); + + // check zero + const id2 = conv.messages[0].id; + const subtree2 = buildSubtree(conv, id2); + expect(subtree2).toEqual([conv.messages[0]]); + + //check full length + const id3 = conv.messages[conv.messages.length - 1].id; + const subtree3 = buildSubtree(conv, id3); + expect(subtree3).toEqual(conv.messages); + }); + + it("should throw an error if the message is not found", async () => { + const convId = await insertLinearBranchConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const id = "not-a-real-id-test"; + + expect(() => buildSubtree(conv, id)).toThrow("Message not found"); + }); + + it("should throw an error if the ancestor is not found", async () => { + const convId = await insertLinearBranchConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const id = "1-1-1-1-2"; + + conv.messages[1].ancestors = ["not-a-real-id-test"]; + + expect(() => buildSubtree(conv, id)).toThrow("Ancestor not found"); + }); + + it("should work on empty conversations", () => { + const conv = { + _id: new ObjectId(), + rootMessageId: undefined, + messages: [], + }; + + const subtree = buildSubtree(conv, "not-a-real-id-test"); + expect(subtree).toEqual([]); + }); + + it("should work for conversation with subtrees", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const subtree = buildSubtree(conv, "1-1-1-1-2"); + expect(subtree).toEqual([conv.messages[0], conv.messages[1]]); + + const subtree2 = buildSubtree(conv, "1-1-1-1-4"); + expect(subtree2).toEqual([ + conv.messages[0], + conv.messages[1], + conv.messages[2], + conv.messages[3], + ]); + + const subtree3 = buildSubtree(conv, "1-1-1-1-6"); + expect(subtree3).toEqual([conv.messages[0], conv.messages[4], conv.messages[5]]); + + const subtree4 = buildSubtree(conv, "1-1-1-1-7"); + expect(subtree4).toEqual([conv.messages[0], conv.messages[4], conv.messages[6]]); + }); +}); diff --git a/src/lib/utils/tree/buildSubtree.ts b/src/lib/utils/tree/buildSubtree.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0ec7ffabc0dfd2e5c35cdaf85c30b2e02ce52a5 --- /dev/null +++ b/src/lib/utils/tree/buildSubtree.ts @@ -0,0 +1,28 @@ +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; + +export function buildSubtree( + conv: Pick, + id: Message["id"] +): Message[] { + if (!conv.rootMessageId) { + if (conv.messages.length === 0) return []; + // legacy conversation slice up to id + const index = conv.messages.findIndex((m) => m.id === id); + if (index === -1) throw new Error("Message not found"); + return conv.messages.slice(0, index + 1); + } else { + // find the message with the right id then create the ancestor tree + const message = conv.messages.find((m) => m.id === id); + if (!message) throw new Error("Message not found"); + + return [ + ...(message.ancestors?.map((ancestorId) => { + const ancestor = conv.messages.find((m) => m.id === ancestorId); + if (!ancestor) throw new Error("Ancestor not found"); + return ancestor; + }) ?? []), + message, + ]; + } +} diff --git a/src/lib/utils/tree/convertLegacyConversation.spec.ts b/src/lib/utils/tree/convertLegacyConversation.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8adc55abac3c7bc084fa7935fd94a4be5c776a0 --- /dev/null +++ b/src/lib/utils/tree/convertLegacyConversation.spec.ts @@ -0,0 +1,31 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { describe, expect, it } from "vitest"; + +import { convertLegacyConversation } from "./convertLegacyConversation"; +import { insertLegacyConversation } from "./treeHelpers.spec"; + +describe("convertLegacyConversation", () => { + it("should convert a legacy conversation", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const newConv = convertLegacyConversation(conv); + + expect(newConv.rootMessageId).toBe(newConv.messages[0].id); + expect(newConv.messages[0].ancestors).toEqual([]); + expect(newConv.messages[1].ancestors).toEqual([newConv.messages[0].id]); + expect(newConv.messages[0].children).toEqual([newConv.messages[1].id]); + }); + it("should work on empty conversations", async () => { + const conv = { + _id: new ObjectId(), + rootMessageId: undefined, + messages: [], + }; + const newConv = convertLegacyConversation(conv); + expect(newConv.rootMessageId).toBe(undefined); + expect(newConv.messages).toEqual([]); + }); +}); diff --git a/src/lib/utils/tree/convertLegacyConversation.ts b/src/lib/utils/tree/convertLegacyConversation.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b14468a6296203b53c56889b93d892afa1ffd37 --- /dev/null +++ b/src/lib/utils/tree/convertLegacyConversation.ts @@ -0,0 +1,36 @@ +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import { v4 } from "uuid"; + +export function convertLegacyConversation( + conv: Pick +): Pick { + if (conv.rootMessageId) return conv; // not a legacy conversation + if (conv.messages.length === 0) return conv; // empty conversation + const messages = [ + { + from: "system", + content: conv.preprompt ?? "", + createdAt: new Date(), + updatedAt: new Date(), + id: v4(), + } satisfies Message, + ...conv.messages, + ]; + + const rootMessageId = messages[0].id; + + const newMessages = messages.map((message, index) => { + return { + ...message, + ancestors: messages.slice(0, index).map((m) => m.id), + children: index < messages.length - 1 ? [messages[index + 1].id] : [], + }; + }); + + return { + ...conv, + rootMessageId, + messages: newMessages, + }; +} diff --git a/src/lib/utils/tree/isMessageId.spec.ts b/src/lib/utils/tree/isMessageId.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..91b2baef576d9a73e1effe4e8c7df8b3f67d1e1d --- /dev/null +++ b/src/lib/utils/tree/isMessageId.spec.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { isMessageId } from "./isMessageId"; +import { v4 } from "uuid"; + +describe("isMessageId", () => { + it("should return true for a valid message id", () => { + expect(isMessageId(v4())).toBe(true); + }); + it("should return false for an invalid message id", () => { + expect(isMessageId("1-2-3-4")).toBe(false); + }); + it("should return false for an empty string", () => { + expect(isMessageId("")).toBe(false); + }); +}); diff --git a/src/lib/utils/tree/isMessageId.ts b/src/lib/utils/tree/isMessageId.ts new file mode 100644 index 0000000000000000000000000000000000000000..e46b4526cac3dcb037471fbc77974e33ee2021a6 --- /dev/null +++ b/src/lib/utils/tree/isMessageId.ts @@ -0,0 +1,5 @@ +import type { Message } from "$lib/types/Message"; + +export function isMessageId(id: string): id is Message["id"] { + return id.split("-").length === 5; +} diff --git a/src/lib/utils/tree/treeHelpers.spec.ts b/src/lib/utils/tree/treeHelpers.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..75ce04927c45176077aa30fa270194d52ad12377 --- /dev/null +++ b/src/lib/utils/tree/treeHelpers.spec.ts @@ -0,0 +1,164 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { describe, expect, it } from "vitest"; + +// function used to insert conversations used for testing + +export const insertLegacyConversation = async () => { + const res = await collections.conversations.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + title: "legacy conversation", + model: "", + embeddingModel: "", + messages: [ + { + id: "1-1-1-1-1", + from: "user", + content: "Hello, world! I am a user", + }, + { + id: "1-1-1-1-2", + from: "assistant", + content: "Hello, world! I am an assistant.", + }, + { + id: "1-1-1-1-3", + from: "user", + content: "Hello, world! I am a user.", + }, + { + id: "1-1-1-1-4", + from: "assistant", + content: "Hello, world! I am an assistant.", + }, + ], + }); + return res.insertedId; +}; + +export const insertLinearBranchConversation = async () => { + const res = await collections.conversations.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + title: "linear branch conversation", + model: "", + embeddingModel: "", + + rootMessageId: "1-1-1-1-1", + messages: [ + { + id: "1-1-1-1-1", + from: "user", + content: "Hello, world! I am a user", + ancestors: [], + children: ["1-1-1-1-2"], + }, + { + id: "1-1-1-1-2", + from: "assistant", + content: "Hello, world! I am an assistant.", + ancestors: ["1-1-1-1-1"], + children: ["1-1-1-1-3"], + }, + { + id: "1-1-1-1-3", + from: "user", + content: "Hello, world! I am a user.", + ancestors: ["1-1-1-1-1", "1-1-1-1-2"], + children: ["1-1-1-1-4"], + }, + { + id: "1-1-1-1-4", + from: "assistant", + content: "Hello, world! I am an assistant.", + ancestors: ["1-1-1-1-1", "1-1-1-1-2", "1-1-1-1-3"], + children: [], + }, + ], + }); + return res.insertedId; +}; + +export const insertSideBranchesConversation = async () => { + const res = await collections.conversations.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + title: "side branches conversation", + model: "", + embeddingModel: "", + rootMessageId: "1-1-1-1-1", + messages: [ + { + id: "1-1-1-1-1", + from: "user", + content: "Hello, world, root message!", + ancestors: [], + children: ["1-1-1-1-2", "1-1-1-1-5"], + }, + { + id: "1-1-1-1-2", + from: "assistant", + content: "Hello, response to root message!", + ancestors: ["1-1-1-1-1"], + children: ["1-1-1-1-3"], + }, + { + id: "1-1-1-1-3", + from: "user", + content: "Hello, follow up question!", + ancestors: ["1-1-1-1-1", "1-1-1-1-2"], + children: ["1-1-1-1-4"], + }, + { + id: "1-1-1-1-4", + from: "assistant", + content: "Hello, response from follow up question!", + ancestors: ["1-1-1-1-1", "1-1-1-1-2", "1-1-1-1-3"], + children: [], + }, + { + id: "1-1-1-1-5", + from: "assistant", + content: "Hello, alternative assistant answer!", + ancestors: ["1-1-1-1-1"], + children: ["1-1-1-1-6", "1-1-1-1-7"], + }, + { + id: "1-1-1-1-6", + from: "user", + content: "Hello, follow up question to alternative answer!", + ancestors: ["1-1-1-1-1", "1-1-1-1-5"], + children: [], + }, + { + id: "1-1-1-1-7", + from: "user", + content: "Hello, alternative follow up question to alternative answer!", + ancestors: ["1-1-1-1-1", "1-1-1-1-5"], + children: [], + }, + ], + }); + return res.insertedId; +}; + +describe("inserting conversations", () => { + it("should insert a legacy conversation", async () => { + const id = await insertLegacyConversation(); + expect(id).toBeDefined(); + }); + + it("should insert a linear branch conversation", async () => { + const id = await insertLinearBranchConversation(); + expect(id).toBeDefined(); + }); + + it("should insert a side branches conversation", async () => { + const id = await insertSideBranchesConversation(); + expect(id).toBeDefined(); + }); +}); diff --git a/src/lib/workers/markdownWorker.ts b/src/lib/workers/markdownWorker.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e71f60877ca7e3b2c6a400895a9cb70c1b6817d --- /dev/null +++ b/src/lib/workers/markdownWorker.ts @@ -0,0 +1,53 @@ +import type { WebSearchSource } from "$lib/types/WebSearch"; +import { processTokens, type Token } from "$lib/utils/marked"; + +export type IncomingMessage = { + type: "process"; + content: string; + sources: WebSearchSource[]; +}; + +export type OutgoingMessage = { + type: "processed"; + tokens: Token[]; +}; + +// Flag to track if the worker is currently processing a message +let isProcessing = false; + +// Buffer to store the latest incoming message +let latestMessage: IncomingMessage | null = null; + +// Helper function to safely handle the latest message +async function processMessage() { + if (latestMessage) { + const nextMessage = latestMessage; + + latestMessage = null; + isProcessing = true; + + try { + const { content, sources } = nextMessage; + const processedTokens = await processTokens(content, sources); + postMessage(JSON.parse(JSON.stringify({ type: "processed", tokens: processedTokens }))); + } finally { + isProcessing = false; + + // After processing, check if a new message was buffered + await new Promise((resolve) => setTimeout(resolve, 100)); + processMessage(); + } + } +} + +onmessage = (event) => { + if (event.data.type !== "process") { + return; + } + + latestMessage = event.data as IncomingMessage; + + if (!isProcessing && latestMessage) { + processMessage(); + } +}; diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c0eddca222422904bed308fe355c03d1cf06da91 --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,20 @@ + + +
+
+

{page.status}

+
+

{page.error?.message}

+ {#if page.error?.errorId} +
+
{page.error
+					.errorId}
+ {/if} +
+
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..f2caa374834f5c13eb31cbc87768c893174f8e11 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,284 @@ +import type { LayoutServerLoad } from "./$types"; +import { collections } from "$lib/server/database"; +import type { Conversation } from "$lib/types/Conversation"; +import { UrlDependency } from "$lib/types/UrlDependency"; +import { defaultModel, models, oldModels, validateModel } from "$lib/server/models"; +import { authCondition, requiresUser } from "$lib/server/auth"; +import { DEFAULT_SETTINGS } from "$lib/types/Settings"; +import { env } from "$env/dynamic/private"; +import { ObjectId } from "mongodb"; +import type { ConvSidebar } from "$lib/types/ConvSidebar"; +import { toolFromConfigs } from "$lib/server/tools"; +import { MetricsServer } from "$lib/server/metrics"; +import type { ToolFront, ToolInputFile } from "$lib/types/Tool"; +import { ReviewStatus } from "$lib/types/Review"; +import { base } from "$app/paths"; + +export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { + depends(UrlDependency.ConversationList); + + const settings = await collections.settings.findOne(authCondition(locals)); + + // If the active model in settings is not valid, set it to the default model. This can happen if model was disabled. + if ( + settings && + !validateModel(models).safeParse(settings?.activeModel).success && + !settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel) + ) { + settings.activeModel = defaultModel.id; + await collections.settings.updateOne(authCondition(locals), { + $set: { activeModel: defaultModel.id }, + }); + } + + // if the model is unlisted, set the active model to the default model + if ( + settings?.activeModel && + models.find((m) => m.id === settings?.activeModel)?.unlisted === true + ) { + settings.activeModel = defaultModel.id; + await collections.settings.updateOne(authCondition(locals), { + $set: { activeModel: defaultModel.id }, + }); + } + + const enableAssistants = env.ENABLE_ASSISTANTS === "true"; + + const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? ""); + + const assistant = assistantActive + ? await collections.assistants.findOne({ + _id: new ObjectId(settings?.activeModel), + }) + : null; + + const nConversations = await collections.conversations.countDocuments(authCondition(locals)); + + const conversations = + nConversations === 0 + ? Promise.resolve([]) + : fetch(`${base}/api/conversations`) + .then((res) => res.json()) + .then( + ( + convs: Pick[] + ) => + convs.map((conv) => ({ + ...conv, + updatedAt: new Date(conv.updatedAt), + })) + ); + + const userAssistants = settings?.assistants?.map((assistantId) => assistantId.toString()) ?? []; + const userAssistantsSet = new Set(userAssistants); + + const assistants = conversations.then((conversations) => + collections.assistants + .find({ + _id: { + $in: [ + ...userAssistants.map((el) => new ObjectId(el)), + ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]), + ], + }, + }) + .toArray() + ); + + const messagesBeforeLogin = env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0; + + let loginRequired = false; + + if (requiresUser && !locals.user) { + if (messagesBeforeLogin === 0) { + loginRequired = true; + } else if (nConversations >= messagesBeforeLogin) { + loginRequired = true; + } else { + // get the number of messages where `from === "assistant"` across all conversations. + const totalMessages = + ( + await collections.conversations + .aggregate([ + { $match: { ...authCondition(locals), "messages.from": "assistant" } }, + { $project: { messages: 1 } }, + { $limit: messagesBeforeLogin + 1 }, + { $unwind: "$messages" }, + { $match: { "messages.from": "assistant" } }, + { $count: "messages" }, + ]) + .toArray() + )[0]?.messages ?? 0; + + loginRequired = totalMessages >= messagesBeforeLogin; + } + } + + const toolUseDuration = (await MetricsServer.getMetrics().tool.toolUseDuration.get()).values; + + const configToolIds = toolFromConfigs.map((el) => el._id.toString()); + + let activeCommunityToolIds = (settings?.tools ?? []).filter( + (key) => !configToolIds.includes(key) + ); + + if (assistant) { + activeCommunityToolIds = [...activeCommunityToolIds, ...(assistant.tools ?? [])]; + } + + const communityTools = await collections.tools + .find({ _id: { $in: activeCommunityToolIds.map((el) => new ObjectId(el)) } }) + .toArray() + .then((tools) => + tools.map((tool) => ({ + ...tool, + isHidden: false, + isOnByDefault: true, + isLocked: true, + })) + ); + + return { + nConversations, + conversations: await conversations.then( + async (convs) => + await Promise.all( + convs.map(async (conv) => { + if (settings?.hideEmojiOnSidebar) { + conv.title = conv.title.replace(/\p{Emoji}/gu, ""); + } + + // remove invalid unicode and trim whitespaces + conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart(); + + let avatarUrl: string | undefined = undefined; + + if (conv.assistantId) { + const hash = ( + await collections.assistants.findOne({ + _id: new ObjectId(conv.assistantId), + }) + )?.avatar; + if (hash) { + avatarUrl = `/settings/assistants/${conv.assistantId}/avatar.jpg?hash=${hash}`; + } + } + + return { + id: conv._id.toString(), + title: conv.title, + model: conv.model ?? defaultModel, + updatedAt: conv.updatedAt, + assistantId: conv.assistantId?.toString(), + avatarUrl, + } satisfies ConvSidebar; + }) + ) + ), + settings: { + searchEnabled: !!( + env.SERPAPI_KEY || + env.SERPER_API_KEY || + env.SERPSTACK_API_KEY || + env.SEARCHAPI_KEY || + env.YDC_API_KEY || + env.USE_LOCAL_WEBSEARCH || + env.SEARXNG_QUERY_URL || + env.BING_SUBSCRIPTION_KEY + ), + ethicsModalAccepted: !!settings?.ethicsModalAcceptedAt, + ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null, + activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel, + hideEmojiOnSidebar: settings?.hideEmojiOnSidebar ?? false, + shareConversationsWithModelAuthors: + settings?.shareConversationsWithModelAuthors ?? + DEFAULT_SETTINGS.shareConversationsWithModelAuthors, + customPrompts: settings?.customPrompts ?? {}, + assistants: userAssistants, + tools: + settings?.tools ?? + toolFromConfigs + .filter((el) => !el.isHidden && el.isOnByDefault) + .map((el) => el._id.toString()), + disableStream: settings?.disableStream ?? DEFAULT_SETTINGS.disableStream, + directPaste: settings?.directPaste ?? DEFAULT_SETTINGS.directPaste, + }, + models: models.map((model) => ({ + id: model.id, + name: model.name, + websiteUrl: model.websiteUrl, + modelUrl: model.modelUrl, + tokenizer: model.tokenizer, + datasetName: model.datasetName, + datasetUrl: model.datasetUrl, + displayName: model.displayName, + description: model.description, + reasoning: !!model.reasoning, + logoUrl: model.logoUrl, + promptExamples: model.promptExamples, + parameters: model.parameters, + preprompt: model.preprompt, + multimodal: model.multimodal, + multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes, + tools: model.tools, + unlisted: model.unlisted, + hasInferenceAPI: model.hasInferenceAPI, + })), + oldModels, + tools: [...toolFromConfigs, ...communityTools] + .filter((tool) => !tool?.isHidden) + .map( + (tool) => + ({ + _id: tool._id.toString(), + type: tool.type, + displayName: tool.displayName, + name: tool.name, + description: tool.description, + mimeTypes: (tool.inputs ?? []) + .filter((input): input is ToolInputFile => input.type === "file") + .map((input) => (input as ToolInputFile).mimeTypes) + .flat(), + isOnByDefault: tool.isOnByDefault ?? true, + isLocked: tool.isLocked ?? true, + timeToUseMS: + toolUseDuration.find( + (el) => el.labels.tool === tool._id.toString() && el.labels.quantile === 0.9 + )?.value ?? 15_000, + color: tool.color, + icon: tool.icon, + }) satisfies ToolFront + ), + communityToolCount: await collections.tools.countDocuments({ + type: "community", + review: ReviewStatus.APPROVED, + }), + assistants: assistants.then((assistants) => + assistants + .filter((el) => userAssistantsSet.has(el._id.toString())) + .map((el) => ({ + ...el, + _id: el._id.toString(), + createdById: undefined, + createdByMe: + el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), + })) + ), + user: locals.user && { + id: locals.user._id.toString(), + username: locals.user.username, + avatarUrl: locals.user.avatarUrl, + email: locals.user.email, + logoutDisabled: locals.user.logoutDisabled, + isAdmin: locals.user.isAdmin ?? false, + isEarlyAccess: locals.user.isEarlyAccess ?? false, + }, + assistant: assistant ? JSON.parse(JSON.stringify(assistant)) : null, + enableAssistants, + enableAssistantsRAG: env.ENABLE_ASSISTANTS_RAG === "true", + enableCommunityTools: env.COMMUNITY_TOOLS === "true", + loginRequired, + loginEnabled: requiresUser, + guestMode: requiresUser && messagesBeforeLogin > 0, + }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..76b5567373ff2254011480a3e268fa981a1240ff --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,285 @@ + + + + {envPublic.PUBLIC_APP_NAME} + + + + + + + {#if !$page.url.pathname.includes("/assistant/") && $page.route.id !== "/assistants" && !$page.url.pathname.includes("/models/") && !$page.url.pathname.includes("/tools")} + + + + + + {/if} + + + + + + {#if envPublic.PUBLIC_PLAUSIBLE_SCRIPT_URL && envPublic.PUBLIC_ORIGIN} + + {/if} + + {#if envPublic.PUBLIC_APPLE_APP_ID} + + {/if} + + +{#if showDisclaimer} + ($settings.ethicsModalAccepted = true)} /> +{/if} + +{#if $loginModalOpen} + { + $loginModalOpen = false; + }} + /> +{/if} + +{#if overloadedModalOpen && isHuggingChat} + (overloadedModalOpen = false)} /> +{/if} + +
+ (isNavCollapsed = !isNavCollapsed)} + classNames="absolute inset-y-0 z-10 my-auto {!isNavCollapsed + ? 'left-[290px]' + : 'left-0'} *:transition-transform" + /> + + + shareConversation(ev.detail.id, ev.detail.title)} + on:deleteConversation={(ev) => deleteConversation(ev.detail)} + on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)} + /> + + + {#if currentError} + + {/if} + {@render children?.()} +
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2e1f106febd7bf78c9b18b02010a42ff07bae836 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,103 @@ + + + + {envPublic.PUBLIC_APP_NAME} + + + createConversation(ev.detail)} + {loading} + assistant={data.assistant} + {currentModel} + models={data.models} + bind:files +/> diff --git a/src/routes/admin/export/+server.ts b/src/routes/admin/export/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..7b420ec9f21ad42bf49b52b9734bb179d3e87ec9 --- /dev/null +++ b/src/routes/admin/export/+server.ts @@ -0,0 +1,159 @@ +import { env } from "$env/dynamic/private"; +import { collections } from "$lib/server/database"; +import type { Message } from "$lib/types/Message"; +import { error } from "@sveltejs/kit"; +import { pathToFileURL } from "node:url"; +import { unlink } from "node:fs/promises"; +import { uploadFile } from "@huggingface/hub"; +import parquet from "parquetjs"; +import { z } from "zod"; +import { logger } from "$lib/server/logger.js"; + +// Triger like this: +// curl -X POST "http://localhost:5173/chat/admin/export" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"model": "OpenAssistant/oasst-sft-6-llama-30b-xor"}' + +export async function POST({ request }) { + if (!env.PARQUET_EXPORT_DATASET || !env.PARQUET_EXPORT_HF_TOKEN) { + error(500, "Parquet export is not configured."); + } + + const { model } = z + .object({ + model: z.string(), + }) + .parse(await request.json()); + + const schema = new parquet.ParquetSchema({ + title: { type: "UTF8" }, + created_at: { type: "TIMESTAMP_MILLIS" }, + updated_at: { type: "TIMESTAMP_MILLIS" }, + messages: { + repeated: true, + fields: { + from: { type: "UTF8" }, + content: { type: "UTF8" }, + score: { type: "INT_8", optional: true }, + }, + }, + }); + + const fileName = `/tmp/conversations-${new Date().toJSON().slice(0, 10)}-${Date.now()}.parquet`; + + const writer = await parquet.ParquetWriter.openFile(schema, fileName); + + let count = 0; + logger.info("Exporting conversations for model", model); + + for await (const conversation of collections.settings.aggregate<{ + title: string; + created_at: Date; + updated_at: Date; + messages: Message[]; + }>([ + { + $match: { + shareConversationsWithModelAuthors: true, + sessionId: { $exists: true }, + userId: { $exists: false }, + }, + }, + { + $lookup: { + from: "conversations", + localField: "sessionId", + foreignField: "sessionId", + as: "conversations", + pipeline: [{ $match: { model, userId: { $exists: false } } }], + }, + }, + { $unwind: "$conversations" }, + { + $project: { + title: "$conversations.title", + created_at: "$conversations.createdAt", + updated_at: "$conversations.updatedAt", + messages: "$conversations.messages", + }, + }, + ])) { + await writer.appendRow({ + title: conversation.title, + created_at: conversation.created_at, + updated_at: conversation.updated_at, + messages: conversation.messages.map((message: Message) => ({ + from: message.from, + content: message.content, + ...(message.score ? { score: message.score } : undefined), + })), + }); + ++count; + + if (count % 1_000 === 0) { + logger.info("Exported", count, "conversations"); + } + } + + logger.info("exporting convos with userId"); + + for await (const conversation of collections.settings.aggregate<{ + title: string; + created_at: Date; + updated_at: Date; + messages: Message[]; + }>([ + { $match: { shareConversationsWithModelAuthors: true, userId: { $exists: true } } }, + { + $lookup: { + from: "conversations", + localField: "userId", + foreignField: "userId", + as: "conversations", + pipeline: [{ $match: { model } }], + }, + }, + { $unwind: "$conversations" }, + { + $project: { + title: "$conversations.title", + created_at: "$conversations.createdAt", + updated_at: "$conversations.updatedAt", + messages: "$conversations.messages", + }, + }, + ])) { + await writer.appendRow({ + title: conversation.title, + created_at: conversation.created_at, + updated_at: conversation.updated_at, + messages: conversation.messages.map((message: Message) => ({ + from: message.from, + content: message.content, + ...(message.score ? { score: message.score } : undefined), + })), + }); + ++count; + + if (count % 1_000 === 0) { + logger.info("Exported", count, "conversations"); + } + } + + await writer.close(); + + logger.info("Uploading", fileName, "to Hugging Face Hub"); + + await uploadFile({ + file: pathToFileURL(fileName) as URL, + credentials: { accessToken: env.PARQUET_EXPORT_HF_TOKEN }, + repo: { + type: "dataset", + name: env.PARQUET_EXPORT_DATASET, + }, + }); + + logger.info("Upload done"); + + await unlink(fileName); + + return new Response(); +} diff --git a/src/routes/admin/stats/compute/+server.ts b/src/routes/admin/stats/compute/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef1efe0df17652a0953e3b40b80b5905ab881338 --- /dev/null +++ b/src/routes/admin/stats/compute/+server.ts @@ -0,0 +1,16 @@ +import { json } from "@sveltejs/kit"; +import { logger } from "$lib/server/logger"; +import { computeAllStats } from "$lib/jobs/refresh-conversation-stats"; + +// Triger like this: +// curl -X POST "http://localhost:5173/chat/admin/stats/compute" -H "Authorization: Bearer " + +export async function POST() { + computeAllStats().catch((e) => logger.error(e)); + return json( + { + message: "Stats job started", + }, + { status: 202 } + ); +} diff --git a/src/routes/api/assistant/+server.ts b/src/routes/api/assistant/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..dcf3a383aea73804ac6ef656cac809195d9f66bc --- /dev/null +++ b/src/routes/api/assistant/+server.ts @@ -0,0 +1,108 @@ +import { authCondition } from "$lib/server/auth.js"; +import { requiresUser } from "$lib/server/auth.js"; +import { asssistantSchema } from "./utils.js"; +import { uploadAssistantAvatar } from "./utils.js"; +import { collections } from "$lib/server/database.js"; +import { ObjectId } from "mongodb"; +import sharp from "sharp"; +import { generateSearchTokens } from "$lib/utils/searchTokens"; +import { usageLimits } from "$lib/server/usageLimits.js"; +import { ReviewStatus } from "$lib/types/Review.js"; + +export async function POST({ request, locals }) { + const formData = await request.formData(); + const parse = await asssistantSchema.safeParseAsync(Object.fromEntries(formData)); + + if (!parse.success) { + // Loop through the errors array and create a custom errors array + const errors = parse.error.errors.map((error) => { + return { + field: error.path[0], + message: error.message, + }; + }); + + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + // can only create assistants when logged in, IF login is setup + if (!locals.user && requiresUser) { + const errors = [{ field: "preprompt", message: "Must be logged in. Unauthorized" }]; + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + const createdById = locals.user?._id ?? locals.sessionId; + + const assistantsCount = await collections.assistants.countDocuments({ createdById }); + + if (usageLimits?.assistants && assistantsCount > usageLimits.assistants) { + const errors = [ + { + field: "preprompt", + message: "You have reached the maximum number of assistants. Delete some to continue.", + }, + ]; + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + const newAssistantId = new ObjectId(); + + const exampleInputs: string[] = [ + parse?.data?.exampleInput1 ?? "", + parse?.data?.exampleInput2 ?? "", + parse?.data?.exampleInput3 ?? "", + parse?.data?.exampleInput4 ?? "", + ].filter((input) => !!input); + + let hash; + if (parse.data.avatar && parse.data.avatar instanceof File && parse.data.avatar.size > 0) { + let image; + try { + image = await sharp(await parse.data.avatar.arrayBuffer()) + .resize(512, 512, { fit: "inside" }) + .jpeg({ quality: 80 }) + .toBuffer(); + } catch (e) { + const errors = [{ field: "avatar", message: (e as Error).message }]; + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + hash = await uploadAssistantAvatar(new File([image], "avatar.jpg"), newAssistantId); + } + + const { insertedId } = await collections.assistants.insertOne({ + _id: newAssistantId, + createdById, + createdByName: locals.user?.username ?? locals.user?.name, + ...parse.data, + tools: parse.data.tools, + exampleInputs, + avatar: hash, + createdAt: new Date(), + updatedAt: new Date(), + userCount: 1, + review: ReviewStatus.PRIVATE, + rag: { + allowedLinks: parse.data.ragLinkList, + allowedDomains: parse.data.ragDomainList, + allowAllDomains: parse.data.ragAllowAll, + }, + dynamicPrompt: parse.data.dynamicPrompt, + searchTokens: generateSearchTokens(parse.data.name), + last24HoursCount: 0, + generateSettings: { + temperature: parse.data.temperature, + top_p: parse.data.top_p, + repetition_penalty: parse.data.repetition_penalty, + top_k: parse.data.top_k, + }, + }); + + // add insertedId to user settings + + await collections.settings.updateOne(authCondition(locals), { + $addToSet: { assistants: insertedId }, + }); + + return new Response(JSON.stringify({ success: true, assistantId: insertedId }), { status: 200 }); +} diff --git a/src/routes/api/assistant/[id]/+server.ts b/src/routes/api/assistant/[id]/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..63ad8e5f67e3165e9f2a018de32be9f2dcb3c524 --- /dev/null +++ b/src/routes/api/assistant/[id]/+server.ts @@ -0,0 +1,179 @@ +import { collections } from "$lib/server/database"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { asssistantSchema, uploadAssistantAvatar } from "../utils.js"; +import { requiresUser } from "$lib/server/auth.js"; +import sharp from "sharp"; +import { generateSearchTokens } from "$lib/utils/searchTokens"; + +export async function GET({ params }) { + const id = params.id; + const assistantId = new ObjectId(id); + + const assistant = await collections.assistants.findOne({ + _id: assistantId, + }); + + if (assistant) { + return Response.json(assistant); + } else { + return Response.json({ message: "Assistant not found" }, { status: 404 }); + } +} + +export async function PATCH({ request, locals, params }) { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(params.id), + }); + + if (!assistant) { + throw Error("Assistant not found"); + } + + if (assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString()) { + throw Error("You are not the author of this assistant"); + } + + const formData = Object.fromEntries(await request.formData()); + + const parse = await asssistantSchema.safeParseAsync(formData); + + if (!parse.success) { + // Loop through the errors array and create a custom errors array + const errors = parse.error.errors.map((error) => { + return { + field: error.path[0], + message: error.message, + }; + }); + + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + // can only create assistants when logged in, IF login is setup + if (!locals.user && requiresUser) { + const errors = [{ field: "preprompt", message: "Must be logged in. Unauthorized" }]; + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + const exampleInputs: string[] = [ + parse?.data?.exampleInput1 ?? "", + parse?.data?.exampleInput2 ?? "", + parse?.data?.exampleInput3 ?? "", + parse?.data?.exampleInput4 ?? "", + ].filter((input) => !!input); + + const deleteAvatar = parse.data.avatar === "null"; + + let hash; + if (parse.data.avatar && parse.data.avatar !== "null" && parse.data.avatar.size > 0) { + let image; + try { + image = await sharp(await parse.data.avatar.arrayBuffer()) + .resize(512, 512, { fit: "inside" }) + .jpeg({ quality: 80 }) + .toBuffer(); + } catch (e) { + const errors = [{ field: "avatar", message: (e as Error).message }]; + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + const fileCursor = collections.bucket.find({ filename: assistant._id.toString() }); + + // Step 2: Delete the existing file if it exists + let fileId = await fileCursor.next(); + while (fileId) { + await collections.bucket.delete(fileId._id); + fileId = await fileCursor.next(); + } + + hash = await uploadAssistantAvatar(new File([image], "avatar.jpg"), assistant._id); + } else if (deleteAvatar) { + // delete the avatar + const fileCursor = collections.bucket.find({ filename: assistant._id.toString() }); + + let fileId = await fileCursor.next(); + while (fileId) { + await collections.bucket.delete(fileId._id); + fileId = await fileCursor.next(); + } + } + + const { acknowledged } = await collections.assistants.updateOne( + { + _id: assistant._id, + }, + { + $set: { + name: parse.data.name, + description: parse.data.description, + modelId: parse.data.modelId, + preprompt: parse.data.preprompt, + exampleInputs, + avatar: deleteAvatar ? undefined : (hash ?? assistant.avatar), + updatedAt: new Date(), + rag: { + allowedLinks: parse.data.ragLinkList, + allowedDomains: parse.data.ragDomainList, + allowAllDomains: parse.data.ragAllowAll, + }, + tools: parse.data.tools, + dynamicPrompt: parse.data.dynamicPrompt, + searchTokens: generateSearchTokens(parse.data.name), + generateSettings: { + temperature: parse.data.temperature, + top_p: parse.data.top_p, + repetition_penalty: parse.data.repetition_penalty, + top_k: parse.data.top_k, + }, + }, + } + ); + + if (acknowledged) { + return new Response(JSON.stringify({ success: true, assistantId: assistant._id }), { + status: 200, + }); + } else { + return new Response(JSON.stringify({ error: true, message: "Update failed" }), { status: 500 }); + } +} + +export async function DELETE({ params, locals }) { + const assistant = await collections.assistants.findOne({ _id: new ObjectId(params.id) }); + + if (!assistant) { + return error(404, "Assistant not found"); + } + + if ( + assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString() && + !locals.user?.isAdmin + ) { + return error(403, "You are not the author of this assistant"); + } + + await collections.assistants.deleteOne({ _id: assistant._id }); + + // and remove it from all users settings + await collections.settings.updateMany( + { + assistants: { $in: [assistant._id] }, + }, + { + $pull: { assistants: assistant._id }, + } + ); + + // and delete all avatars + const fileCursor = collections.bucket.find({ filename: assistant._id.toString() }); + + // Step 2: Delete the existing file if it exists + let fileId = await fileCursor.next(); + while (fileId) { + await collections.bucket.delete(fileId._id); + fileId = await fileCursor.next(); + } + + return new Response("Assistant deleted", { status: 200 }); +} diff --git a/src/routes/api/assistant/[id]/report/+server.ts b/src/routes/api/assistant/[id]/report/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5372fbdaf1e9e5cc4cfcdd967c46ba6327c0a66 --- /dev/null +++ b/src/routes/api/assistant/[id]/report/+server.ts @@ -0,0 +1,65 @@ +import { base } from "$app/paths"; + +import { collections } from "$lib/server/database"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { z } from "zod"; + +import { env } from "$env/dynamic/private"; +import { env as envPublic } from "$env/dynamic/public"; +import { sendSlack } from "$lib/server/sendSlack"; +import type { Assistant } from "$lib/types/Assistant"; + +export async function POST({ params, request, locals, url }) { + // is there already a report from this user for this model ? + const report = await collections.reports.findOne({ + createdBy: locals.user?._id ?? locals.sessionId, + object: "assistant", + contentId: new ObjectId(params.id), + }); + + if (report) { + return error(400, "Already reported"); + } + + const { reason } = z.object({ reason: z.string().min(1).max(128) }).parse(await request.json()); + + if (!reason) { + return error(400, "Invalid report reason"); + } + + const { acknowledged } = await collections.reports.insertOne({ + _id: new ObjectId(), + contentId: new ObjectId(params.id), + object: "assistant", + createdBy: locals.user?._id ?? locals.sessionId, + createdAt: new Date(), + updatedAt: new Date(), + reason, + }); + + if (!acknowledged) { + return error(500, "Failed to report assistant"); + } + + if (env.WEBHOOK_URL_REPORT_ASSISTANT) { + const prefixUrl = + envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`; + const assistantUrl = `${prefixUrl}/assistant/${params.id}`; + + const assistant = await collections.assistants.findOne>( + { _id: new ObjectId(params.id) }, + { projection: { name: 1 } } + ); + + const username = locals.user?.username; + + await sendSlack( + `🔴 Assistant <${assistantUrl}|${assistant?.name}> reported by ${ + username ? `` : "non-logged in user" + }.\n\n> ${reason}` + ); + } + + return new Response("Assistant reported", { status: 200 }); +} diff --git a/src/routes/api/assistant/[id]/review/+server.ts b/src/routes/api/assistant/[id]/review/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e37e5e6e560aa0b298784b1aff63ac0711d41ae --- /dev/null +++ b/src/routes/api/assistant/[id]/review/+server.ts @@ -0,0 +1,75 @@ +import { collections } from "$lib/server/database"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { base } from "$app/paths"; +import { env as envPublic } from "$env/dynamic/public"; +import { ReviewStatus } from "$lib/types/Review"; +import { sendSlack } from "$lib/server/sendSlack"; +import { z } from "zod"; + +const schema = z.object({ + status: z.nativeEnum(ReviewStatus), +}); + +export async function PATCH({ params, request, locals, url }) { + const assistantId = params.id; + + const { status } = schema.parse(await request.json()); + + if (!assistantId) { + return error(400, "Assistant ID is required"); + } + + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(assistantId), + }); + + if (!assistant) { + return error(404, "Assistant not found"); + } + + if ( + !locals.user || + (!locals.user.isAdmin && assistant.createdById.toString() !== locals.user._id.toString()) + ) { + return error(403, "Permission denied"); + } + + // only admins can set the status to APPROVED or DENIED + // if the status is already APPROVED or DENIED, only admins can change it + + if ( + (status === ReviewStatus.APPROVED || + status === ReviewStatus.DENIED || + assistant.review === ReviewStatus.APPROVED || + assistant.review === ReviewStatus.DENIED) && + !locals.user?.isAdmin + ) { + return error(403, "Permission denied"); + } + + const result = await collections.assistants.updateOne( + { _id: assistant._id }, + { $set: { review: status } } + ); + + if (result.modifiedCount === 0) { + return error(500, "Failed to update review status"); + } + + if (status === ReviewStatus.PENDING) { + const prefixUrl = + envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`; + const assistantUrl = `${prefixUrl}/assistant/${assistantId}`; + + const username = locals.user?.username; + + await sendSlack( + `🟢 Assistant <${assistantUrl}|${assistant?.name}> requested to be featured by ${ + username ? `` : "non-logged in user" + }.` + ); + } + + return new Response("Review status updated", { status: 200 }); +} diff --git a/src/routes/api/assistant/[id]/subscribe/+server.ts b/src/routes/api/assistant/[id]/subscribe/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe67acf644db8985c37c7f54c98730957da395f5 --- /dev/null +++ b/src/routes/api/assistant/[id]/subscribe/+server.ts @@ -0,0 +1,64 @@ +import { authCondition } from "$lib/server/auth"; + +import { collections } from "$lib/server/database"; +import { defaultModel } from "$lib/server/models.js"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +export async function POST({ params, locals }) { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(params.id), + }); + + if (!assistant) { + return error(404, "Assistant not found"); + } + + // don't push if it's already there + const settings = await collections.settings.findOne(authCondition(locals)); + + if (settings?.assistants?.includes(assistant._id)) { + return error(400, "Already subscribed"); + } + + const result = await collections.settings.updateOne(authCondition(locals), { + $addToSet: { assistants: assistant._id }, + }); + + // reduce count only if push succeeded + if (result.modifiedCount > 0) { + await collections.assistants.updateOne({ _id: assistant._id }, { $inc: { userCount: 1 } }); + } + + return new Response("Assistant subscribed", { status: 200 }); +} + +export async function DELETE({ params, locals }) { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(params.id), + }); + + if (!assistant) { + return error(404, "Assistant not found"); + } + + const result = await collections.settings.updateOne(authCondition(locals), { + $pull: { assistants: assistant._id }, + }); + + // reduce count only if pull succeeded + if (result.modifiedCount > 0) { + await collections.assistants.updateOne({ _id: assistant._id }, { $inc: { userCount: -1 } }); + } + + const settings = await collections.settings.findOne(authCondition(locals)); + + // if the assistant was the active model, set the default model as active + if (settings?.activeModel === assistant._id.toString()) { + await collections.settings.updateOne(authCondition(locals), { + $set: { activeModel: defaultModel.id }, + }); + } + + return new Response("Assistant unsubscribed", { status: 200 }); +} diff --git a/src/routes/api/assistant/utils.ts b/src/routes/api/assistant/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f26f526b959294e7bd9ca14a0d794f426b10e33 --- /dev/null +++ b/src/routes/api/assistant/utils.ts @@ -0,0 +1,71 @@ +import { parseStringToList } from "$lib/utils/parseStringToList"; +import { toolFromConfigs } from "$lib/server/tools"; +import { z } from "zod"; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { sha256 } from "$lib/utils/sha256"; + +export const asssistantSchema = z.object({ + name: z.string().min(1), + modelId: z.string().min(1), + preprompt: z.string().min(1), + description: z.string().optional(), + exampleInput1: z.string().optional(), + exampleInput2: z.string().optional(), + exampleInput3: z.string().optional(), + exampleInput4: z.string().optional(), + avatar: z.union([z.instanceof(File), z.literal("null")]).optional(), + ragLinkList: z.preprocess(parseStringToList, z.string().url().array().max(10)), + ragDomainList: z.preprocess(parseStringToList, z.string().array()), + ragAllowAll: z.preprocess((v) => v === "true", z.boolean()), + dynamicPrompt: z.preprocess((v) => v === "on", z.boolean()), + temperature: z + .union([z.literal(""), z.coerce.number().min(0.1).max(2)]) + .transform((v) => (v === "" ? undefined : v)), + top_p: z + .union([z.literal(""), z.coerce.number().min(0.05).max(1)]) + .transform((v) => (v === "" ? undefined : v)), + + repetition_penalty: z + .union([z.literal(""), z.coerce.number().min(0.1).max(2)]) + .transform((v) => (v === "" ? undefined : v)), + + top_k: z + .union([z.literal(""), z.coerce.number().min(5).max(100)]) + .transform((v) => (v === "" ? undefined : v)), + tools: z + .string() + .optional() + .transform((v) => (v ? v.split(",") : [])) + .transform(async (v) => [ + ...(await collections.tools + .find({ _id: { $in: v.map((toolId) => new ObjectId(toolId)) } }) + .project({ _id: 1 }) + .toArray() + .then((tools) => tools.map((tool) => tool._id.toString()))), + ...toolFromConfigs + .filter((el) => (v ?? []).includes(el._id.toString())) + .map((el) => el._id.toString()), + ]) + .optional(), +}); + +export const uploadAssistantAvatar = async ( + avatar: File, + assistantId: ObjectId +): Promise => { + const hash = await sha256(await avatar.text()); + const upload = collections.bucket.openUploadStream(`${assistantId.toString()}`, { + metadata: { type: avatar.type, hash }, + }); + + upload.write((await avatar.arrayBuffer()) as unknown as Buffer); + upload.end(); + + // only return the filename when upload throws a finish event or a 10s time out occurs + return new Promise((resolve, reject) => { + upload.once("finish", () => resolve(hash)); + upload.once("error", reject); + setTimeout(() => reject(new Error("Upload timed out")), 10000); + }); +}; diff --git a/src/routes/api/assistants/+server.ts b/src/routes/api/assistants/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d80dec50855dec797ba73a551e37d8afbf46124 --- /dev/null +++ b/src/routes/api/assistants/+server.ts @@ -0,0 +1,65 @@ +import { collections } from "$lib/server/database"; +import type { Assistant } from "$lib/types/Assistant"; +import type { User } from "$lib/types/User"; +import { generateQueryTokens } from "$lib/utils/searchTokens.js"; +import type { Filter } from "mongodb"; +import { env } from "$env/dynamic/private"; +import { ReviewStatus } from "$lib/types/Review"; + +const NUM_PER_PAGE = 24; + +export async function GET({ url, locals }) { + const modelId = url.searchParams.get("modelId"); + const pageIndex = parseInt(url.searchParams.get("p") ?? "0"); + const username = url.searchParams.get("user"); + const query = url.searchParams.get("q")?.trim() ?? null; + const showUnfeatured = url.searchParams.get("showUnfeatured") === "true"; + const createdByCurrentUser = locals.user?.username && locals.user.username === username; + + let user: Pick | null = null; + if (username) { + user = await collections.users.findOne>( + { username }, + { projection: { _id: 1 } } + ); + if (!user) { + return Response.json({ message: `User "${username}" doesn't exist` }, { status: 404 }); + } + } + + // if we require featured assistants, that we are not on a user page and we are not an admin who wants to see unfeatured assistants, we show featured assistants + let shouldBeFeatured = {}; + + if (env.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.user?.isAdmin && showUnfeatured)) { + if (!user) { + // only show featured assistants on the community page + shouldBeFeatured = { review: ReviewStatus.APPROVED }; + } else if (!createdByCurrentUser) { + // on a user page show assistants that have been approved or are pending + shouldBeFeatured = { review: { $in: [ReviewStatus.APPROVED, ReviewStatus.PENDING] } }; + } + } + // fetch the top assistants sorted by user count from biggest to smallest, filter out all assistants with only 1 users. filter by model too if modelId is provided + const filter: Filter = { + ...(modelId && { modelId }), + ...(user && { createdById: user._id }), + ...(query && { searchTokens: { $all: generateQueryTokens(query) } }), + ...shouldBeFeatured, + }; + const assistants = await collections.assistants + .find(filter) + .skip(NUM_PER_PAGE * pageIndex) + .sort({ userCount: -1 }) + .limit(NUM_PER_PAGE) + .toArray(); + + const numTotalItems = await collections.assistants.countDocuments(filter); + + return Response.json({ + assistants, + selectedModel: modelId ?? "", + numTotalItems, + numItemsPerPage: NUM_PER_PAGE, + query, + }); +} diff --git a/src/routes/api/conversation/[id]/+server.ts b/src/routes/api/conversation/[id]/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..d879373b8a7cb340d466d167b9e1b036d0c1d7f3 --- /dev/null +++ b/src/routes/api/conversation/[id]/+server.ts @@ -0,0 +1,44 @@ +import { collections } from "$lib/server/database"; +import { authCondition } from "$lib/server/auth"; +import { z } from "zod"; +import { models } from "$lib/server/models"; +import { ObjectId } from "mongodb"; + +export async function GET({ locals, params }) { + const id = z.string().parse(params.id); + const convId = new ObjectId(id); + + if (locals.user?._id || locals.sessionId) { + const conv = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (conv) { + const res = { + id: conv._id, + title: conv.title, + updatedAt: conv.updatedAt, + modelId: conv.model, + assistantId: conv.assistantId, + messages: conv.messages.map((message) => ({ + content: message.content, + from: message.from, + id: message.id, + createdAt: message.createdAt, + updatedAt: message.updatedAt, + webSearch: message.webSearch, + files: message.files, + updates: message.updates, + reasoning: message.reasoning, + })), + modelTools: models.find((m) => m.id == conv.model)?.tools ?? false, + }; + return Response.json(res); + } else { + return Response.json({ message: "Conversation not found" }, { status: 404 }); + } + } else { + return Response.json({ message: "Must have session cookie" }, { status: 401 }); + } +} diff --git a/src/routes/api/conversation/[id]/message/[messageId]/+server.ts b/src/routes/api/conversation/[id]/message/[messageId]/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..80d642ca31274b4c9cbc410cb9f0bd9df64af836 --- /dev/null +++ b/src/routes/api/conversation/[id]/message/[messageId]/+server.ts @@ -0,0 +1,42 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +export async function DELETE({ locals, params }) { + const messageId = params.messageId; + + if (!messageId || typeof messageId !== "string") { + error(400, "Invalid message id"); + } + + const conversation = await collections.conversations.findOne({ + ...authCondition(locals), + _id: new ObjectId(params.id), + }); + + if (!conversation) { + error(404, "Conversation not found"); + } + + const filteredMessages = conversation.messages + .filter( + (message) => + // not the message AND the message is not in ancestors + !(message.id === messageId) && message.ancestors && !message.ancestors.includes(messageId) + ) + .map((message) => { + // remove the message from children if it's there + if (message.children && message.children.includes(messageId)) { + message.children = message.children.filter((child) => child !== messageId); + } + return message; + }); + + await collections.conversations.updateOne( + { _id: conversation._id, ...authCondition(locals) }, + { $set: { messages: filteredMessages } } + ); + + return new Response(); +} diff --git a/src/routes/api/conversations/+server.ts b/src/routes/api/conversations/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d0eb796948aee227b9b60075cd72e798884e7ad --- /dev/null +++ b/src/routes/api/conversations/+server.ts @@ -0,0 +1,54 @@ +import { collections } from "$lib/server/database"; +import { models } from "$lib/server/models"; +import { authCondition } from "$lib/server/auth"; +import type { Conversation } from "$lib/types/Conversation"; +import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination"; + +export async function GET({ locals, url }) { + const p = parseInt(url.searchParams.get("p") ?? "0"); + + if (locals.user?._id || locals.sessionId) { + const convs = await collections.conversations + .find({ + ...authCondition(locals), + }) + .project>({ + title: 1, + updatedAt: 1, + model: 1, + assistantId: 1, + }) + .sort({ updatedAt: -1 }) + .skip(p * CONV_NUM_PER_PAGE) + .limit(CONV_NUM_PER_PAGE) + .toArray(); + + if (convs.length === 0) { + return Response.json([]); + } + + const res = convs.map((conv) => ({ + _id: conv._id, + id: conv._id, // legacy param iOS + title: conv.title, + updatedAt: conv.updatedAt, + model: conv.model, + modelId: conv.model, // legacy param iOS + assistantId: conv.assistantId, + modelTools: models.find((m) => m.id == conv.model)?.tools ?? false, + })); + return Response.json(res); + } else { + return Response.json({ message: "Must have session cookie" }, { status: 401 }); + } +} + +export async function DELETE({ locals }) { + if (locals.user?._id || locals.sessionId) { + await collections.conversations.deleteMany({ + ...authCondition(locals), + }); + } + + return new Response(); +} diff --git a/src/routes/api/models/+server.ts b/src/routes/api/models/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..d29c0253888b69003868e0529055c1bf5fe15772 --- /dev/null +++ b/src/routes/api/models/+server.ts @@ -0,0 +1,25 @@ +import { models } from "$lib/server/models"; + +export async function GET() { + const res = models + .filter((m) => m.unlisted == false) + .map((model) => ({ + id: model.id, + name: model.name, + websiteUrl: model.websiteUrl ?? "https://huggingface.co", + modelUrl: model.modelUrl ?? "https://huggingface.co", + tokenizer: model.tokenizer, + datasetName: model.datasetName, + datasetUrl: model.datasetUrl, + displayName: model.displayName, + description: model.description ?? "", + logoUrl: model.logoUrl, + promptExamples: model.promptExamples ?? [], + preprompt: model.preprompt ?? "", + multimodal: model.multimodal ?? false, + unlisted: model.unlisted ?? false, + tools: model.tools ?? false, + hasInferenceAPI: model.hasInferenceAPI ?? false, + })); + return Response.json(res); +} diff --git a/src/routes/api/spaces-config/+server.ts b/src/routes/api/spaces-config/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..00b960bb4f51acde38f0319ad729688a59b6923b --- /dev/null +++ b/src/routes/api/spaces-config/+server.ts @@ -0,0 +1,45 @@ +import { env } from "$env/dynamic/private"; +import { Client } from "@gradio/client"; + +export async function GET({ url }) { + if (env.COMMUNITY_TOOLS !== "true") { + return new Response("Community tools are not enabled", { status: 403 }); + } + + const space = url.searchParams.get("space"); + + if (!space) { + return new Response("Missing space", { status: 400 }); + } + // Extract namespace from space URL or use as-is if it's already in namespace format + let namespace = null; + if (space.startsWith("https://huggingface.co/spaces/")) { + namespace = space.split("/").slice(-2).join("/"); + } else if (space.match(/^[^/]+\/[^/]+$/)) { + namespace = space; + } + + if (!namespace) { + return new Response( + "Invalid space name. Specify a namespace or a full URL on huggingface.co.", + { status: 400 } + ); + } + + try { + const api = await (await Client.connect(namespace)).view_api(); + return new Response(JSON.stringify(api), { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }); + } catch (e) { + return new Response("Error fetching space API. Is the name correct?", { + status: 400, + headers: { + "Content-Type": "application/json", + }, + }); + } +} diff --git a/src/routes/api/tools/+server.ts b/src/routes/api/tools/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffc30aa7959c513fbde5305fe3f04b176853c71a --- /dev/null +++ b/src/routes/api/tools/+server.ts @@ -0,0 +1,68 @@ +import { env } from "$env/dynamic/private"; +import { authCondition, requiresUser } from "$lib/server/auth.js"; +import { collections } from "$lib/server/database.js"; +import { editableToolSchema } from "$lib/server/tools/index.js"; +import { generateSearchTokens } from "$lib/utils/searchTokens.js"; +import { ObjectId } from "mongodb"; +import { ReviewStatus } from "$lib/types/Review.js"; +import { error } from "@sveltejs/kit"; +import { usageLimits } from "$lib/server/usageLimits.js"; + +export async function POST({ request, locals }) { + if (env.COMMUNITY_TOOLS !== "true") { + error(403, "Community tools are not enabled"); + } + const body = await request.json(); + + const parse = editableToolSchema.safeParse(body); + + if (!parse.success) { + // Loop through the errors array and create a custom errors array + const errors = parse.error.errors.map((error) => { + return { + field: error.path[0], + message: error.message, + }; + }); + + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + // can only create tools when logged in, IF login is setup + if (!locals.user && requiresUser) { + const errors = [{ field: "description", message: "Must be logged in. Unauthorized" }]; + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + const toolCounts = await collections.tools.countDocuments({ createdById: locals.user?._id }); + + if (usageLimits?.tools && toolCounts > usageLimits.tools) { + const errors = [ + { + field: "description", + message: "You have reached the maximum number of tools. Delete some to continue.", + }, + ]; + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + if (!locals.user || !authCondition(locals)) { + error(401, "Unauthorized"); + } + + const { insertedId } = await collections.tools.insertOne({ + ...parse.data, + type: "community" as const, + _id: new ObjectId(), + createdById: locals.user?._id, + createdByName: locals.user?.username, + createdAt: new Date(), + updatedAt: new Date(), + last24HoursUseCount: 0, + useCount: 0, + review: ReviewStatus.PRIVATE, + searchTokens: generateSearchTokens(parse.data.displayName), + }); + + return new Response(JSON.stringify({ toolId: insertedId.toString() }), { status: 200 }); +} diff --git a/src/routes/api/tools/[toolId]/+server.ts b/src/routes/api/tools/[toolId]/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..1c7ebdad8f75f8af49c0fd772478dc27b37243bd --- /dev/null +++ b/src/routes/api/tools/[toolId]/+server.ts @@ -0,0 +1,144 @@ +import { env } from "$env/dynamic/private"; +import { collections } from "$lib/server/database.js"; +import { toolFromConfigs } from "$lib/server/tools/index.js"; +import { ReviewStatus } from "$lib/types/Review"; +import type { CommunityToolDB } from "$lib/types/Tool.js"; +import { ObjectId } from "mongodb"; +import { editableToolSchema } from "$lib/server/tools/index.js"; +import { generateSearchTokens } from "$lib/utils/searchTokens.js"; +import { error } from "@sveltejs/kit"; +import { requiresUser } from "$lib/server/auth"; + +export async function GET({ params }) { + if (env.COMMUNITY_TOOLS !== "true") { + return new Response("Community tools are not enabled", { status: 403 }); + } + + const toolId = params.toolId; + + try { + const configTool = toolFromConfigs.find((el) => el._id.toString() === toolId); + if (configTool) { + return Response.json({ + _id: toolId, + displayName: configTool.displayName, + color: configTool.color, + icon: configTool.icon, + createdByName: undefined, + }); + } else { + // try community tools + const tool = await collections.tools + .findOne({ _id: new ObjectId(toolId) }) + .then((tool) => + tool + ? { + _id: tool._id.toString(), + displayName: tool.displayName, + color: tool.color, + icon: tool.icon, + createdByName: tool.createdByName, + review: tool.review, + } + : undefined + ); + + if (!tool || tool.review !== ReviewStatus.APPROVED) { + return new Response(`Tool "${toolId}" not found`, { status: 404 }); + } + + return Response.json(tool); + } + } catch (e) { + return new Response(`Tool "${toolId}" not found`, { status: 404 }); + } +} + +export async function PATCH({ request, params, locals }) { + const tool = await collections.tools.findOne({ + _id: new ObjectId(params.toolId), + }); + + if (!tool) { + error(404, "Tool not found"); + } + + if (tool.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString()) { + error(403, "You are not the creator of this tool"); + } + + // can only create tools when logged in, IF login is setup + if (!locals.user && requiresUser) { + const errors = [{ field: "description", message: "Must be logged in. Unauthorized" }]; + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + const body = await request.json(); + + const parse = editableToolSchema.safeParse(body); + + if (!parse.success) { + // Loop through the errors array and create a custom errors array + const errors = parse.error.errors.map((error) => { + return { + field: error.path[0], + message: error.message, + }; + }); + + return new Response(JSON.stringify({ error: true, errors }), { status: 400 }); + } + + // modify the tool + await collections.tools.updateOne( + { _id: tool._id }, + { + $set: { + ...parse.data, + updatedAt: new Date(), + searchTokens: generateSearchTokens(parse.data.displayName), + }, + } + ); + + return new Response(JSON.stringify({ toolId: tool._id.toString() }), { status: 200 }); +} + +export async function DELETE({ params, locals }) { + const tool = await collections.tools.findOne({ _id: new ObjectId(params.toolId) }); + + if (!tool) { + return new Response("Tool not found", { status: 404 }); + } + + if ( + tool.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString() && + !locals.user?.isAdmin + ) { + return new Response("You are not the creator of this tool", { status: 403 }); + } + + await collections.tools.deleteOne({ _id: tool._id }); + + // Remove the tool from all users' settings + await collections.settings.updateMany( + { + tools: { $in: [tool._id.toString()] }, + }, + { + $pull: { tools: tool._id.toString() }, + } + ); + + // Remove the tool from all assistants + await collections.assistants.updateMany( + { + tools: { $in: [tool._id.toString()] }, + }, + { + $pull: { tools: tool._id.toString() }, + } + ); + + return new Response("Tool deleted", { status: 200 }); +} diff --git a/src/routes/api/tools/[toolId]/report/+server.ts b/src/routes/api/tools/[toolId]/report/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..80b23c942b600df53f31fb73113645e7f134c3de --- /dev/null +++ b/src/routes/api/tools/[toolId]/report/+server.ts @@ -0,0 +1,65 @@ +import { base } from "$app/paths"; + +import { collections } from "$lib/server/database"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { z } from "zod"; + +import { env } from "$env/dynamic/private"; +import { env as envPublic } from "$env/dynamic/public"; +import { sendSlack } from "$lib/server/sendSlack"; +import type { Tool } from "$lib/types/Tool"; + +export async function POST({ params, request, locals, url }) { + // is there already a report from this user for this model ? + const report = await collections.reports.findOne({ + createdBy: locals.user?._id ?? locals.sessionId, + object: "tool", + contentId: new ObjectId(params.toolId), + }); + + if (report) { + return error(400, "Already reported"); + } + + const { reason } = z.object({ reason: z.string().min(1).max(128) }).parse(await request.json()); + + if (!reason) { + return error(400, "Invalid report reason"); + } + + const { acknowledged } = await collections.reports.insertOne({ + _id: new ObjectId(), + contentId: new ObjectId(params.toolId), + object: "tool", + createdBy: locals.user?._id ?? locals.sessionId, + createdAt: new Date(), + updatedAt: new Date(), + reason, + }); + + if (!acknowledged) { + return error(500, "Failed to report tool"); + } + + if (env.WEBHOOK_URL_REPORT_ASSISTANT) { + const prefixUrl = + envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`; + const toolUrl = `${prefixUrl}/tools/${params.toolId}`; + + const tool = await collections.tools.findOne>( + { _id: new ObjectId(params.toolId) }, + { projection: { displayName: 1, name: 1 } } + ); + + const username = locals.user?.username; + + await sendSlack( + `🔴 Tool <${toolUrl}|${tool?.displayName ?? tool?.name}> reported by ${ + username ? `` : "non-logged in user" + }.\n\n> ${reason}` + ); + } + + return new Response("Tool reported", { status: 200 }); +} diff --git a/src/routes/api/tools/[toolId]/review/+server.ts b/src/routes/api/tools/[toolId]/review/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..88a0a772efa3decbdf212aa7bf6373b13a77f922 --- /dev/null +++ b/src/routes/api/tools/[toolId]/review/+server.ts @@ -0,0 +1,72 @@ +import { collections } from "$lib/server/database"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { base } from "$app/paths"; +import { env as envPublic } from "$env/dynamic/public"; +import { ReviewStatus } from "$lib/types/Review"; +import { sendSlack } from "$lib/server/sendSlack"; +import { z } from "zod"; + +const schema = z.object({ + status: z.nativeEnum(ReviewStatus), +}); + +export async function PATCH({ params, request, locals, url }) { + const toolId = params.toolId; + + const { status } = schema.parse(await request.json()); + + if (!toolId) { + return error(400, "Tool ID is required"); + } + + const tool = await collections.tools.findOne({ + _id: new ObjectId(toolId), + }); + + if (!tool) { + return error(404, "Tool not found"); + } + + if ( + !locals.user || + (!locals.user.isAdmin && tool.createdById.toString() !== locals.user._id.toString()) + ) { + return error(403, "Permission denied"); + } + + // only admins can set the status to APPROVED or DENIED + // if the status is already APPROVED or DENIED, only admins can change it + + if ( + (status === ReviewStatus.APPROVED || + status === ReviewStatus.DENIED || + tool.review === ReviewStatus.APPROVED || + tool.review === ReviewStatus.DENIED) && + !locals.user?.isAdmin + ) { + return error(403, "Permission denied"); + } + + const result = await collections.tools.updateOne({ _id: tool._id }, { $set: { review: status } }); + + if (result.modifiedCount === 0) { + return error(500, "Failed to update review status"); + } + + if (status === ReviewStatus.PENDING) { + const prefixUrl = + envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`; + const toolUrl = `${prefixUrl}/tools/${toolId}`; + + const username = locals.user?.username; + + await sendSlack( + `🟢🛠️ Tool <${toolUrl}|${tool?.displayName}> requested to be featured by ${ + username ? `` : "non-logged in user" + }.` + ); + } + + return new Response("Review status updated", { status: 200 }); +} diff --git a/src/routes/api/tools/search/+server.ts b/src/routes/api/tools/search/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..3baf366cf5aff8e9edf6380a3202d0e76cf1d319 --- /dev/null +++ b/src/routes/api/tools/search/+server.ts @@ -0,0 +1,58 @@ +import { env } from "$env/dynamic/private"; +import { collections } from "$lib/server/database.js"; +import { toolFromConfigs } from "$lib/server/tools/index.js"; +import type { BaseTool, CommunityToolDB } from "$lib/types/Tool.js"; +import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens.js"; +import type { Filter } from "mongodb"; +import { ReviewStatus } from "$lib/types/Review"; +export async function GET({ url }) { + if (env.COMMUNITY_TOOLS !== "true") { + return new Response("Community tools are not enabled", { status: 403 }); + } + + const query = url.searchParams.get("q")?.trim() ?? null; + const queryTokens = !!query && generateQueryTokens(query); + + const filter: Filter = { + ...(queryTokens && { searchTokens: { $all: queryTokens } }), + review: ReviewStatus.APPROVED, + }; + + const matchingCommunityTools = await collections.tools + .find(filter) + .project>({ + _id: 1, + displayName: 1, + color: 1, + icon: 1, + createdByName: 1, + }) + .sort({ useCount: -1 }) + .limit(5) + .toArray(); + + const matchingConfigTools = toolFromConfigs + .filter((tool) => !tool?.isHidden) + .filter((tool) => tool.name !== "websearch") // filter out websearch tool from config tools since its added separately + .filter((tool) => { + if (queryTokens) { + return generateSearchTokens(tool.displayName).some((token) => + queryTokens.some((queryToken) => queryToken.test(token)) + ); + } + return true; + }) + .map((tool) => ({ + _id: tool._id, + displayName: tool.displayName, + color: tool.color, + icon: tool.icon, + createdByName: undefined, + })); + + const tools = [...matchingConfigTools, ...matchingCommunityTools] satisfies Array< + Pick & { createdByName?: string } + >; + + return Response.json(tools.map((tool) => ({ ...tool, _id: tool._id.toString() })).slice(0, 5)); +} diff --git a/src/routes/api/user/+server.ts b/src/routes/api/user/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d848372beb54691954471b42abbc557d9b52726 --- /dev/null +++ b/src/routes/api/user/+server.ts @@ -0,0 +1,15 @@ +export async function GET({ locals }) { + if (locals.user) { + const res = { + id: locals.user._id, + username: locals.user.username, + name: locals.user.name, + email: locals.user.email, + avatarUrl: locals.user.avatarUrl, + hfUserId: locals.user.hfUserId, + }; + + return Response.json(res); + } + return Response.json({ message: "Must be signed in" }, { status: 401 }); +} diff --git a/src/routes/api/user/assistants/+server.ts b/src/routes/api/user/assistants/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e8b66bf7817114f3901f512b092a5b93e06edf1 --- /dev/null +++ b/src/routes/api/user/assistants/+server.ts @@ -0,0 +1,43 @@ +import { authCondition } from "$lib/server/auth"; +import type { Conversation } from "$lib/types/Conversation"; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; + +export async function GET({ locals }) { + if (locals.user?._id || locals.sessionId) { + const settings = await collections.settings.findOne(authCondition(locals)); + + const conversations = await collections.conversations + .find(authCondition(locals)) + .sort({ updatedAt: -1 }) + .project>({ + assistantId: 1, + }) + .limit(300) + .toArray(); + + const userAssistants = settings?.assistants?.map((assistantId) => assistantId.toString()) ?? []; + const userAssistantsSet = new Set(userAssistants); + + const assistantIds = [ + ...userAssistants.map((el) => new ObjectId(el)), + ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]), + ]; + + const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray(); + + const res = assistants + .filter((el) => userAssistantsSet.has(el._id.toString())) + .map((el) => ({ + ...el, + _id: el._id.toString(), + createdById: undefined, + createdByMe: + el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), + })); + + return Response.json(res); + } else { + return Response.json({ message: "Must have session cookie" }, { status: 401 }); + } +} diff --git a/src/routes/assistant/[assistantId]/+page.server.ts b/src/routes/assistant/[assistantId]/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..072b3516c8ea8f72dbdd9491aae7d2b24ff38510 --- /dev/null +++ b/src/routes/assistant/[assistantId]/+page.server.ts @@ -0,0 +1,42 @@ +import { base } from "$app/paths"; +import { collections } from "$lib/server/database"; +import { redirect } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { authCondition } from "$lib/server/auth.js"; + +export async function load({ params, locals }) { + try { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(params.assistantId), + }); + + if (!assistant) { + redirect(302, `${base}`); + } + + if (locals.user?._id ?? locals.sessionId) { + await collections.settings.updateOne( + authCondition(locals), + { + $set: { + activeModel: assistant._id.toString(), + updatedAt: new Date(), + }, + $push: { assistants: assistant._id }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { + upsert: true, + } + ); + } + + return { + assistant: JSON.parse(JSON.stringify(assistant)), + }; + } catch { + redirect(302, `${base}`); + } +} diff --git a/src/routes/assistant/[assistantId]/+page.svelte b/src/routes/assistant/[assistantId]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1760c0849b1d0b9457937524df16b96220a7e02b --- /dev/null +++ b/src/routes/assistant/[assistantId]/+page.svelte @@ -0,0 +1,93 @@ + + + + + + + + + + + + createConversation(ev.detail)} + {loading} + currentModel={findCurrentModel([...data.models, ...data.oldModels], data.assistant.modelId)} + assistant={data.assistant} + models={data.models} + bind:files +/> diff --git a/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts b/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..3513750d0449f70bc2d4ca865c15c30e5af0f8d3 --- /dev/null +++ b/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts @@ -0,0 +1,83 @@ +import ChatThumbnail from "./ChatThumbnail.svelte"; +import { collections } from "$lib/server/database"; +import { error, type RequestHandler } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { render } from "svelte/server"; + +import { Resvg } from "@resvg/resvg-js"; +import satori from "satori"; +import { html } from "satori-html"; + +import InterRegular from "$lib/server/fonts/Inter-Regular.ttf"; +import InterBold from "$lib/server/fonts/Inter-Bold.ttf"; +import sharp from "sharp"; + +export const GET: RequestHandler = (async ({ params }) => { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(params.assistantId), + }); + + if (!assistant) { + error(404, "Assistant not found."); + } + + let avatar = ""; + const fileId = collections.bucket.find({ filename: assistant._id.toString() }); + const file = await fileId.next(); + if (file) { + avatar = await (async () => { + const fileStream = collections.bucket.openDownloadStream(file?._id); + + const fileBuffer = await new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + fileStream.on("data", (chunk) => chunks.push(chunk)); + fileStream.on("error", reject); + fileStream.on("end", () => resolve(Buffer.concat(chunks))); + }); + + return fileBuffer; + })() + .then(async (buf) => sharp(buf).jpeg().toBuffer()) // convert to jpeg bc satori png is really slow + .then(async (buf) => "data:image/jpeg;base64," + buf.toString("base64")); + } + + const renderedComponent = render(ChatThumbnail, { + props: { + name: assistant.name, + description: assistant.description, + createdByName: assistant.createdByName, + avatar, + }, + }); + + const reactLike = html("" + renderedComponent.body); + + const svg = await satori(reactLike, { + width: 1200, + height: 648, + fonts: [ + { + name: "Inter", + data: InterRegular as unknown as ArrayBuffer, + weight: 500, + }, + { + name: "Inter", + data: InterBold as unknown as ArrayBuffer, + weight: 700, + }, + ], + }); + + const png = new Resvg(svg, { + fitTo: { mode: "original" }, + }) + .render() + .asPng(); + + return new Response(png, { + headers: { + "Content-Type": "image/png", + }, + }); +}) satisfies RequestHandler; diff --git a/src/routes/assistant/[assistantId]/thumbnail.png/ChatThumbnail.svelte b/src/routes/assistant/[assistantId]/thumbnail.png/ChatThumbnail.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b3b93260c2bcb489467fba45a36dd18cd7b2f9cc --- /dev/null +++ b/src/routes/assistant/[assistantId]/thumbnail.png/ChatThumbnail.svelte @@ -0,0 +1,43 @@ + + +
+
+ {#if avatar} + avatar + {/if} +
+

+ + + {@html logo} + + AI assistant +

+

+ {name} +

+

+ {description.slice(0, 160)} + {#if description.length > 160}...{/if} +

+
+ Start chatting +
+
+
+ {#if createdByName} +

+ An AI assistant created by {createdByName} +

+ {/if} +
diff --git a/src/routes/assistants/+page.server.ts b/src/routes/assistants/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..c185bf198becc7d1e09477a609d74561b39fa117 --- /dev/null +++ b/src/routes/assistants/+page.server.ts @@ -0,0 +1,83 @@ +import { base } from "$app/paths"; +import { env } from "$env/dynamic/private"; +import { collections } from "$lib/server/database.js"; +import { SortKey, type Assistant } from "$lib/types/Assistant"; +import type { User } from "$lib/types/User"; +import { generateQueryTokens } from "$lib/utils/searchTokens.js"; +import { error, redirect } from "@sveltejs/kit"; +import type { Filter } from "mongodb"; +import { ReviewStatus } from "$lib/types/Review"; +const NUM_PER_PAGE = 24; + +export const load = async ({ url, locals }) => { + if (!env.ENABLE_ASSISTANTS) { + redirect(302, `${base}/`); + } + + const modelId = url.searchParams.get("modelId"); + const pageIndex = parseInt(url.searchParams.get("p") ?? "0"); + const username = url.searchParams.get("user"); + const query = url.searchParams.get("q")?.trim() ?? null; + const sort = url.searchParams.get("sort")?.trim() ?? SortKey.TRENDING; + const showUnfeatured = url.searchParams.get("showUnfeatured") === "true"; + const createdByCurrentUser = locals.user?.username && locals.user.username === username; + + let user: Pick | null = null; + if (username) { + user = await collections.users.findOne>( + { username }, + { projection: { _id: 1 } } + ); + if (!user) { + error(404, `User "${username}" doesn't exist`); + } + } + + // if we require featured assistants, that we are not on a user page and we are not an admin who wants to see unfeatured assistants, we show featured assistants + let shouldBeFeatured = {}; + + if (env.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.user?.isAdmin && showUnfeatured)) { + if (!user) { + // only show featured assistants on the community page + shouldBeFeatured = { review: ReviewStatus.APPROVED }; + } else if (!createdByCurrentUser) { + // on a user page show assistants that have been approved or are pending + shouldBeFeatured = { review: { $in: [ReviewStatus.APPROVED, ReviewStatus.PENDING] } }; + } + } + + const noSpecificSearch = !user && !query; + // fetch the top assistants sorted by user count from biggest to smallest. + // filter by model too if modelId is provided or query if query is provided + // only show assistants that have been used by more than 5 users if no specific search is made + const filter: Filter = { + ...(modelId && { modelId }), + ...(user && { createdById: user._id }), + ...(query && { searchTokens: { $all: generateQueryTokens(query) } }), + ...(noSpecificSearch && { userCount: { $gte: 5 } }), + ...shouldBeFeatured, + }; + + const assistants = await collections.assistants + .find(filter) + .sort({ + ...(sort === SortKey.TRENDING && { last24HoursCount: -1 }), + userCount: -1, + _id: 1, + }) + .skip(NUM_PER_PAGE * pageIndex) + .limit(NUM_PER_PAGE) + .toArray(); + + const numTotalItems = await collections.assistants.countDocuments(filter); + + return { + assistants: JSON.parse(JSON.stringify(assistants)) as Array, + selectedModel: modelId ?? "", + numTotalItems, + numItemsPerPage: NUM_PER_PAGE, + query, + sort, + showUnfeatured, + }; +}; diff --git a/src/routes/assistants/+page.svelte b/src/routes/assistants/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e3034b1e907d1e52cedef7c0e3ac53060440deec --- /dev/null +++ b/src/routes/assistants/+page.svelte @@ -0,0 +1,351 @@ + + + + {#if isHuggingChat} + HuggingChat - Assistants + + + + + + {/if} + + +
+
+
+

Assistants

+ {#if isHuggingChat} +
+ beta +
+ + + + {/if} +
+

Popular assistants made by the community

+
+ + {#if data.user?.isAdmin} + + {/if} + {#if page.data.loginRequired && !data.user} + + {:else} + + Create new assistant + + {/if} +
+ +
+ {#if assistantsCreator && !createdByMe} +
+ {assistantsCreator}'s Assistants + +
+ {#if isHuggingChat} + View {assistantsCreator} + on HF + {/if} + {:else} + + + Community + + {#if data.user?.username} + {data.user.username} + + {/if} + {/if} +
+ + filterOnName(e.currentTarget.value)} + bind:this={filterInputEl} + maxlength="150" + type="search" + aria-label="Filter assistants by name" + /> +
+ +
+ +
+ {#each data.assistants as assistant (assistant._id)} + {@const hasRag = + assistant?.rag?.allowAllDomains || + !!assistant?.rag?.allowedDomains?.length || + !!assistant?.rag?.allowedLinks?.length || + !!assistant?.dynamicPrompt} + + + {:else} + No assistants found + {/each} +
+ +
+
diff --git a/src/routes/conversation/+server.ts b/src/routes/conversation/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..fd0ebc6b83e8575454b9eda9cdd595f980d10388 --- /dev/null +++ b/src/routes/conversation/+server.ts @@ -0,0 +1,128 @@ +import type { RequestHandler } from "./$types"; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { error, redirect } from "@sveltejs/kit"; +import { base } from "$app/paths"; +import { z } from "zod"; +import type { Message } from "$lib/types/Message"; +import { models, validateModel } from "$lib/server/models"; +import { defaultEmbeddingModel } from "$lib/server/embeddingModels"; +import { v4 } from "uuid"; +import { authCondition } from "$lib/server/auth"; +import { usageLimits } from "$lib/server/usageLimits"; +import { MetricsServer } from "$lib/server/metrics"; + +export const POST: RequestHandler = async ({ locals, request }) => { + const body = await request.text(); + + let title = ""; + + const parsedBody = z + .object({ + fromShare: z.string().optional(), + model: validateModel(models), + assistantId: z.string().optional(), + preprompt: z.string().optional(), + }) + .safeParse(JSON.parse(body)); + + if (!parsedBody.success) { + error(400, "Invalid request"); + } + const values = parsedBody.data; + + const convCount = await collections.conversations.countDocuments(authCondition(locals)); + + if (usageLimits?.conversations && convCount > usageLimits?.conversations) { + error(429, "You have reached the maximum number of conversations. Delete some to continue."); + } + + const model = models.find((m) => (m.id || m.name) === values.model); + + if (!model) { + error(400, "Invalid model"); + } + + let messages: Message[] = [ + { + id: v4(), + from: "system", + content: values.preprompt ?? "", + createdAt: new Date(), + updatedAt: new Date(), + children: [], + ancestors: [], + }, + ]; + + let rootMessageId: Message["id"] = messages[0].id; + let embeddingModel: string; + + if (values.fromShare) { + const conversation = await collections.sharedConversations.findOne({ + _id: values.fromShare, + }); + + if (!conversation) { + error(404, "Conversation not found"); + } + + title = conversation.title; + messages = conversation.messages; + rootMessageId = conversation.rootMessageId ?? rootMessageId; + values.model = conversation.model; + values.preprompt = conversation.preprompt; + values.assistantId = conversation.assistantId?.toString(); + embeddingModel = conversation.embeddingModel; + } + + embeddingModel ??= model.embeddingModel ?? defaultEmbeddingModel.name; + + if (model.unlisted) { + error(400, "Can't start a conversation with an unlisted model"); + } + + // get preprompt from assistant if it exists + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(values.assistantId), + }); + + if (assistant) { + values.preprompt = assistant.preprompt; + } else { + values.preprompt ??= model?.preprompt ?? ""; + } + + if (messages && messages.length > 0 && messages[0].from === "system") { + messages[0].content = values.preprompt; + } + + const res = await collections.conversations.insertOne({ + _id: new ObjectId(), + title: title || "New Chat", + rootMessageId, + messages, + model: values.model, + preprompt: values.preprompt, + assistantId: values.assistantId ? new ObjectId(values.assistantId) : undefined, + createdAt: new Date(), + updatedAt: new Date(), + userAgent: request.headers.get("User-Agent") ?? undefined, + embeddingModel, + ...(locals.user ? { userId: locals.user._id } : { sessionId: locals.sessionId }), + ...(values.fromShare ? { meta: { fromShareId: values.fromShare } } : {}), + }); + + MetricsServer.getMetrics().model.conversationsTotal.inc({ model: values.model }); + + return new Response( + JSON.stringify({ + conversationId: res.insertedId.toString(), + }), + { headers: { "Content-Type": "application/json" } } + ); +}; + +export const GET: RequestHandler = async () => { + redirect(302, `${base}/`); +}; diff --git a/src/routes/conversation/[id]/+page.server.ts b/src/routes/conversation/[id]/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd431a07a6360d150175974ba67a280a7037fcd4 --- /dev/null +++ b/src/routes/conversation/[id]/+page.server.ts @@ -0,0 +1,68 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { error } from "@sveltejs/kit"; +import { authCondition } from "$lib/server/auth"; +import { UrlDependency } from "$lib/types/UrlDependency"; +import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation.js"; + +export const load = async ({ params, depends, locals }) => { + let conversation; + let shared = false; + + // if the conver + if (params.id.length === 7) { + // shared link of length 7 + conversation = await collections.sharedConversations.findOne({ + _id: params.id, + }); + shared = true; + + if (!conversation) { + error(404, "Conversation not found"); + } + } else { + // todo: add validation on params.id + conversation = await collections.conversations.findOne({ + _id: new ObjectId(params.id), + ...authCondition(locals), + }); + + depends(UrlDependency.Conversation); + + if (!conversation) { + const conversationExists = + (await collections.conversations.countDocuments({ + _id: new ObjectId(params.id), + })) !== 0; + + if (conversationExists) { + error( + 403, + "You don't have access to this conversation. If someone gave you this link, ask them to use the 'share' feature instead." + ); + } + + error(404, "Conversation not found."); + } + } + + const convertedConv = { ...conversation, ...convertLegacyConversation(conversation) }; + + return { + messages: convertedConv.messages, + title: convertedConv.title, + model: convertedConv.model, + preprompt: convertedConv.preprompt, + rootMessageId: convertedConv.rootMessageId, + assistant: convertedConv.assistantId + ? JSON.parse( + JSON.stringify( + await collections.assistants.findOne({ + _id: new ObjectId(convertedConv.assistantId), + }) + ) + ) + : null, + shared, + }; +}; diff --git a/src/routes/conversation/[id]/+page.svelte b/src/routes/conversation/[id]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d774a84228215078731d56bd43b7a22081f80869 --- /dev/null +++ b/src/routes/conversation/[id]/+page.svelte @@ -0,0 +1,501 @@ + + + + {title} + + + voteMessage(event.detail.score, event.detail.id)} + on:share={() => shareConversation(page.params.id, data.title)} + on:stop={() => (($isAborted = true), (loading = false))} + models={data.models} + currentModel={findCurrentModel([...data.models, ...data.oldModels], data.model)} + assistant={data.assistant} +/> diff --git a/src/routes/conversation/[id]/+server.ts b/src/routes/conversation/[id]/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..8895ce463d3302c5972eae802c09cf1a11df3e25 --- /dev/null +++ b/src/routes/conversation/[id]/+server.ts @@ -0,0 +1,566 @@ +import { env } from "$env/dynamic/private"; +import { startOfHour } from "date-fns"; +import { authCondition, requiresUser } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { models, validModelIdSchema } from "$lib/server/models"; +import { ERROR_MESSAGES } from "$lib/stores/errors"; +import type { Message } from "$lib/types/Message"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { z } from "zod"; +import { + MessageReasoningUpdateType, + MessageUpdateStatus, + MessageUpdateType, + type MessageUpdate, +} from "$lib/types/MessageUpdate"; +import { uploadFile } from "$lib/server/files/uploadFile"; +import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation"; +import { isMessageId } from "$lib/utils/tree/isMessageId"; +import { buildSubtree } from "$lib/utils/tree/buildSubtree.js"; +import { addChildren } from "$lib/utils/tree/addChildren.js"; +import { addSibling } from "$lib/utils/tree/addSibling.js"; +import { usageLimits } from "$lib/server/usageLimits"; +import { MetricsServer } from "$lib/server/metrics"; +import { textGeneration } from "$lib/server/textGeneration"; +import type { TextGenerationContext } from "$lib/server/textGeneration/types"; +import { logger } from "$lib/server/logger.js"; +import { documentParserToolId } from "$lib/utils/toolIds.js"; + +export async function POST({ request, locals, params, getClientAddress }) { + const id = z.string().parse(params.id); + const convId = new ObjectId(id); + const promptedAt = new Date(); + + const userId = locals.user?._id ?? locals.sessionId; + + // check user + if (!userId) { + error(401, "Unauthorized"); + } + + // check if the user has access to the conversation + const convBeforeCheck = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (convBeforeCheck && !convBeforeCheck.rootMessageId) { + const res = await collections.conversations.updateOne( + { + _id: convId, + }, + { + $set: { + ...convBeforeCheck, + ...convertLegacyConversation(convBeforeCheck), + }, + } + ); + + if (!res.acknowledged) { + error(500, "Failed to convert conversation"); + } + } + + const conv = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (!conv) { + error(404, "Conversation not found"); + } + + // register the event for ratelimiting + await collections.messageEvents.insertOne({ + userId, + createdAt: new Date(), + ip: getClientAddress(), + }); + + const messagesBeforeLogin = env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0; + + // guest mode check + if (!locals.user?._id && requiresUser && messagesBeforeLogin) { + const totalMessages = + ( + await collections.conversations + .aggregate([ + { $match: { ...authCondition(locals), "messages.from": "assistant" } }, + { $project: { messages: 1 } }, + { $limit: messagesBeforeLogin + 1 }, + { $unwind: "$messages" }, + { $match: { "messages.from": "assistant" } }, + { $count: "messages" }, + ]) + .toArray() + )[0]?.messages ?? 0; + + if (totalMessages > messagesBeforeLogin) { + error(429, "Exceeded number of messages before login"); + } + } + + if (usageLimits?.messagesPerMinute) { + // check if the user is rate limited + const nEvents = Math.max( + await collections.messageEvents.countDocuments({ + userId, + createdAt: { $gte: new Date(Date.now() - 60_000) }, + }), + await collections.messageEvents.countDocuments({ + ip: getClientAddress(), + createdAt: { $gte: new Date(Date.now() - 60_000) }, + }) + ); + if (nEvents > usageLimits.messagesPerMinute) { + error(429, ERROR_MESSAGES.rateLimited); + } + } + + if (usageLimits?.messages && conv.messages.length > usageLimits.messages) { + error( + 429, + `This conversation has more than ${usageLimits.messages} messages. Start a new one to continue` + ); + } + + // fetch the model + const model = models.find((m) => m.id === conv.model); + + if (!model) { + error(410, "Model not available anymore"); + } + + // finally parse the content of the request + const form = await request.formData(); + + const json = form.get("data"); + + if (!json || typeof json !== "string") { + error(400, "Invalid request"); + } + + const { + inputs: newPrompt, + id: messageId, + is_retry: isRetry, + is_continue: isContinue, + web_search: webSearch, + tools: toolsPreferences, + } = z + .object({ + id: z.string().uuid().refine(isMessageId).optional(), // parent message id to append to for a normal message, or the message id for a retry/continue + inputs: z.optional( + z + .string() + .min(1) + .transform((s) => s.replace(/\r\n/g, "\n")) + ), + is_retry: z.optional(z.boolean()), + is_continue: z.optional(z.boolean()), + web_search: z.optional(z.boolean()), + tools: z.array(z.string()).optional(), + files: z.optional( + z.array( + z.object({ + type: z.literal("base64").or(z.literal("hash")), + name: z.string(), + value: z.string(), + mime: z.string(), + }) + ) + ), + }) + .parse(JSON.parse(json)); + + const inputFiles = await Promise.all( + form + .getAll("files") + .filter((entry): entry is File => entry instanceof File && entry.size > 0) + .map(async (file) => { + const [type, ...name] = file.name.split(";"); + + return { + type: z.literal("base64").or(z.literal("hash")).parse(type), + value: await file.text(), + mime: file.type, + name: name.join(";"), + }; + }) + ); + + // Check for PDF files in the input + const hasPdfFiles = inputFiles?.some((file) => file.mime === "application/pdf") ?? false; + + // Check for existing PDF files in the conversation + const hasPdfInConversation = + conv.messages?.some((msg) => msg.files?.some((file) => file.mime === "application/pdf")) ?? + false; + + if (usageLimits?.messageLength && (newPrompt?.length ?? 0) > usageLimits.messageLength) { + error(400, "Message too long."); + } + + // each file is either: + // base64 string requiring upload to the server + // hash pointing to an existing file + const hashFiles = inputFiles?.filter((file) => file.type === "hash") ?? []; + const b64Files = + inputFiles + ?.filter((file) => file.type !== "hash") + .map((file) => { + const blob = Buffer.from(file.value, "base64"); + return new File([blob], file.name, { type: file.mime }); + }) ?? []; + + // check sizes + // todo: make configurable + if (b64Files.some((file) => file.size > 10 * 1024 * 1024)) { + error(413, "File too large, should be <10MB"); + } + + const uploadedFiles = await Promise.all(b64Files.map((file) => uploadFile(file, conv))).then( + (files) => [...files, ...hashFiles] + ); + + // we will append tokens to the content of this message + let messageToWriteToId: Message["id"] | undefined = undefined; + // used for building the prompt, subtree of the conversation that goes from the latest message to the root + let messagesForPrompt: Message[] = []; + + if (isContinue && messageId) { + // if it's the last message and we continue then we build the prompt up to the last message + // we will strip the end tokens afterwards when the prompt is built + if ((conv.messages.find((msg) => msg.id === messageId)?.children?.length ?? 0) > 0) { + error(400, "Can only continue the last message"); + } + messageToWriteToId = messageId; + messagesForPrompt = buildSubtree(conv, messageId); + } else if (isRetry && messageId) { + // two cases, if we're retrying a user message with a newPrompt set, + // it means we're editing a user message + // if we're retrying on an assistant message, newPrompt cannot be set + // it means we're retrying the last assistant message for a new answer + + const messageToRetry = conv.messages.find((message) => message.id === messageId); + + if (!messageToRetry) { + error(404, "Message not found"); + } + + if (messageToRetry.from === "user" && newPrompt) { + // add a sibling to this message from the user, with the alternative prompt + // add a children to that sibling, where we can write to + const newUserMessageId = addSibling( + conv, + { + from: "user", + content: newPrompt, + files: uploadedFiles, + createdAt: new Date(), + updatedAt: new Date(), + }, + messageId + ); + messageToWriteToId = addChildren( + conv, + { + from: "assistant", + content: "", + createdAt: new Date(), + updatedAt: new Date(), + }, + newUserMessageId + ); + messagesForPrompt = buildSubtree(conv, newUserMessageId); + } else if (messageToRetry.from === "assistant") { + // we're retrying an assistant message, to generate a new answer + // just add a sibling to the assistant answer where we can write to + messageToWriteToId = addSibling( + conv, + { from: "assistant", content: "", createdAt: new Date(), updatedAt: new Date() }, + messageId + ); + messagesForPrompt = buildSubtree(conv, messageId); + messagesForPrompt.pop(); // don't need the latest assistant message in the prompt since we're retrying it + } + } else { + // just a normal linear conversation, so we add the user message + // and the blank assistant message back to back + const newUserMessageId = addChildren( + conv, + { + from: "user", + content: newPrompt ?? "", + files: uploadedFiles, + createdAt: new Date(), + updatedAt: new Date(), + }, + messageId + ); + + messageToWriteToId = addChildren( + conv, + { + from: "assistant", + content: "", + createdAt: new Date(), + updatedAt: new Date(), + }, + newUserMessageId + ); + // build the prompt from the user message + messagesForPrompt = buildSubtree(conv, newUserMessageId); + } + + const messageToWriteTo = conv.messages.find((message) => message.id === messageToWriteToId); + if (!messageToWriteTo) { + error(500, "Failed to create message"); + } + if (messagesForPrompt.length === 0) { + error(500, "Failed to create prompt"); + } + + // update the conversation with the new messages + await collections.conversations.updateOne( + { _id: convId }, + { $set: { messages: conv.messages, title: conv.title, updatedAt: new Date() } } + ); + + let doneStreaming = false; + + let lastTokenTimestamp: undefined | Date = undefined; + + // we now build the stream + const stream = new ReadableStream({ + async start(controller) { + messageToWriteTo.updates ??= []; + async function update(event: MessageUpdate) { + if (!messageToWriteTo || !conv) { + throw Error("No message or conversation to write events to"); + } + + // Add token to content or skip if empty + if (event.type === MessageUpdateType.Stream) { + if (event.token === "") return; + messageToWriteTo.content += event.token; + + // add to token total + MetricsServer.getMetrics().model.tokenCountTotal.inc({ model: model?.id }); + + // if this is the first token, add to time to first token + if (!lastTokenTimestamp) { + MetricsServer.getMetrics().model.timeToFirstToken.observe( + { model: model?.id }, + Date.now() - promptedAt.getTime() + ); + lastTokenTimestamp = new Date(); + } + + // add to time per token + MetricsServer.getMetrics().model.timePerOutputToken.observe( + { model: model?.id }, + Date.now() - (lastTokenTimestamp ?? promptedAt).getTime() + ); + lastTokenTimestamp = new Date(); + } else if ( + event.type === MessageUpdateType.Reasoning && + event.subtype === MessageReasoningUpdateType.Stream + ) { + messageToWriteTo.reasoning ??= ""; + messageToWriteTo.reasoning += event.token; + } + + // Set the title + else if (event.type === MessageUpdateType.Title) { + conv.title = event.title; + await collections.conversations.updateOne( + { _id: convId }, + { $set: { title: conv?.title, updatedAt: new Date() } } + ); + } + + // Set the final text and the interrupted flag + else if (event.type === MessageUpdateType.FinalAnswer) { + messageToWriteTo.interrupted = event.interrupted; + messageToWriteTo.content = initialMessageContent + event.text; + + // add to latency + MetricsServer.getMetrics().model.latency.observe( + { model: model?.id }, + Date.now() - promptedAt.getTime() + ); + } + + // Add file + else if (event.type === MessageUpdateType.File) { + messageToWriteTo.files = [ + ...(messageToWriteTo.files ?? []), + { type: "hash", name: event.name, value: event.sha, mime: event.mime }, + ]; + } + + // Append to the persistent message updates if it's not a stream update + if ( + event.type !== MessageUpdateType.Stream && + !( + event.type === MessageUpdateType.Status && + event.status === MessageUpdateStatus.KeepAlive + ) && + !( + event.type === MessageUpdateType.Reasoning && + event.subtype === MessageReasoningUpdateType.Stream + ) + ) { + messageToWriteTo?.updates?.push(event); + } + + // Avoid remote keylogging attack executed by watching packet lengths + // by padding the text with null chars to a fixed length + // https://cdn.arstechnica.net/wp-content/uploads/2024/03/LLM-Side-Channel.pdf + if (event.type === MessageUpdateType.Stream) { + event = { ...event, token: event.token.padEnd(16, "\0") }; + } + + // Send the update to the client + controller.enqueue(JSON.stringify(event) + "\n"); + + // Send 4096 of spaces to make sure the browser doesn't blocking buffer that holding the response + if (event.type === MessageUpdateType.FinalAnswer) { + controller.enqueue(" ".repeat(4096)); + } + } + + await collections.conversations.updateOne( + { _id: convId }, + { $set: { title: conv.title, updatedAt: new Date() } } + ); + messageToWriteTo.updatedAt = new Date(); + + let hasError = false; + const initialMessageContent = messageToWriteTo.content; + + try { + const ctx: TextGenerationContext = { + model, + endpoint: await model.getEndpoint(), + conv, + messages: messagesForPrompt, + assistant: undefined, + isContinue: isContinue ?? false, + webSearch: webSearch ?? false, + toolsPreference: [ + ...(toolsPreferences ?? []), + ...(hasPdfFiles || hasPdfInConversation ? [documentParserToolId] : []), // Add document parser tool if PDF files are present + ], + promptedAt, + ip: getClientAddress(), + username: locals.user?.username, + }; + // run the text generation and send updates to the client + for await (const event of textGeneration(ctx)) await update(event); + } catch (e) { + hasError = true; + await update({ + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Error, + message: (e as Error).message, + }); + logger.error(e); + } finally { + // check if no output was generated + if (!hasError && messageToWriteTo.content === initialMessageContent) { + await update({ + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Error, + message: "No output was generated. Something went wrong.", + }); + } + } + + await collections.conversations.updateOne( + { _id: convId }, + { $set: { messages: conv.messages, title: conv?.title, updatedAt: new Date() } } + ); + + // used to detect if cancel() is called bc of interrupt or just because the connection closes + doneStreaming = true; + + controller.close(); + }, + async cancel() { + if (doneStreaming) return; + await collections.conversations.updateOne( + { _id: convId }, + { $set: { messages: conv.messages, title: conv.title, updatedAt: new Date() } } + ); + }, + }); + + if (conv.assistantId) { + await collections.assistantStats.updateOne( + { assistantId: conv.assistantId, "date.at": startOfHour(new Date()), "date.span": "hour" }, + { $inc: { count: 1 } }, + { upsert: true } + ); + } + + const metrics = MetricsServer.getMetrics(); + metrics.model.messagesTotal.inc({ model: model?.id }); + // Todo: maybe we should wait for the message to be saved before ending the response - in case of errors + return new Response(stream, { + headers: { + "Content-Type": "application/jsonl", + }, + }); +} + +export async function DELETE({ locals, params }) { + const convId = new ObjectId(params.id); + + const conv = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (!conv) { + error(404, "Conversation not found"); + } + + await collections.conversations.deleteOne({ _id: conv._id }); + + return new Response(); +} + +export async function PATCH({ request, locals, params }) { + const values = z + .object({ + title: z.string().trim().min(1).max(100).optional(), + model: validModelIdSchema.optional(), + }) + .parse(await request.json()); + + const convId = new ObjectId(params.id); + + const conv = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (!conv) { + error(404, "Conversation not found"); + } + + await collections.conversations.updateOne( + { + _id: convId, + }, + { + $set: values, + } + ); + + return new Response(); +} diff --git a/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts b/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..b43b6f257188f14fdde22a41fa70477b672a9809 --- /dev/null +++ b/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts @@ -0,0 +1,78 @@ +import { buildPrompt } from "$lib/buildPrompt"; +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { models } from "$lib/server/models"; +import { buildSubtree } from "$lib/utils/tree/buildSubtree"; +import { isMessageId } from "$lib/utils/tree/isMessageId"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +export async function GET({ params, locals }) { + const conv = + params.id.length === 7 + ? await collections.sharedConversations.findOne({ + _id: params.id, + }) + : await collections.conversations.findOne({ + _id: new ObjectId(params.id), + ...authCondition(locals), + }); + + if (conv === null) { + error(404, "Conversation not found"); + } + + const messageId = params.messageId; + + const messageIndex = conv.messages.findIndex((msg) => msg.id === messageId); + + if (!isMessageId(messageId) || messageIndex === -1) { + error(404, "Message not found"); + } + + const model = models.find((m) => m.id === conv.model); + + if (!model) { + error(404, "Conversation model not found"); + } + + let assistant; + if (conv.assistantId) { + assistant = await collections.assistants.findOne({ + _id: new ObjectId(conv.assistantId), + }); + } + + const messagesUpTo = buildSubtree(conv, messageId); + + const prompt = await buildPrompt({ + preprompt: conv.preprompt, + messages: messagesUpTo, + model, + }).catch((err) => { + console.error(err); + return "Prompt generation failed"; + }); + + return Response.json({ + prompt, + model: model.name, + assistant: assistant?.name, + parameters: { + ...model.parameters, + ...(assistant?.generateSettings || {}), + return_full_text: false, + }, + messages: messagesUpTo.map((msg) => ({ + role: msg.from, + content: msg.content, + createdAt: msg.createdAt, + updatedAt: msg.updatedAt, + reasoning: msg.reasoning, + updates: msg.updates?.filter( + (u) => (u.type === "webSearch" && u.subtype === "sources") || u.type === "title" + ), + files: msg.files, + })), + }); +} diff --git a/src/routes/conversation/[id]/message/[messageId]/vote/+server.ts b/src/routes/conversation/[id]/message/[messageId]/vote/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb6ada6b7b86e3ec55d857c7fc267545368485ea --- /dev/null +++ b/src/routes/conversation/[id]/message/[messageId]/vote/+server.ts @@ -0,0 +1,58 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { MetricsServer } from "$lib/server/metrics.js"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { z } from "zod"; + +export async function POST({ params, request, locals }) { + const { score } = z + .object({ + score: z.number().int().min(-1).max(1), + }) + .parse(await request.json()); + const conversationId = new ObjectId(params.id); + const messageId = params.messageId; + + // aggregate votes per model in order to detect model performance degradation + const model = await collections.conversations + .findOne( + { + _id: conversationId, + ...authCondition(locals), + }, + { projection: { model: 1 } } + ) + .then((c) => c?.model); + + if (model) { + if (score === 1) { + MetricsServer.getMetrics().model.votesPositive.inc({ model }); + } else { + MetricsServer.getMetrics().model.votesNegative.inc({ model }); + } + } + + const document = await collections.conversations.updateOne( + { + _id: conversationId, + ...authCondition(locals), + "messages.id": messageId, + }, + { + ...(score !== 0 + ? { + $set: { + "messages.$.score": score, + }, + } + : { $unset: { "messages.$.score": "" } }), + } + ); + + if (!document.matchedCount) { + error(404, "Message not found"); + } + + return new Response(); +} diff --git a/src/routes/conversation/[id]/output/[sha256]/+server.ts b/src/routes/conversation/[id]/output/[sha256]/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce78cf9fdf2916cc1c3df49744aa648b9128c304 --- /dev/null +++ b/src/routes/conversation/[id]/output/[sha256]/+server.ts @@ -0,0 +1,58 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { z } from "zod"; +import type { RequestHandler } from "./$types"; +import { downloadFile } from "$lib/server/files/downloadFile"; +import mimeTypes from "mime-types"; + +export const GET: RequestHandler = async ({ locals, params }) => { + const sha256 = z.string().parse(params.sha256); + + const userId = locals.user?._id ?? locals.sessionId; + + // check user + if (!userId) { + error(401, "Unauthorized"); + } + + if (params.id.length !== 7) { + const convId = new ObjectId(z.string().parse(params.id)); + + // check if the user has access to the conversation + const conv = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (!conv) { + error(404, "Conversation not found"); + } + } else { + // look for the conversation in shared conversations + const conv = await collections.sharedConversations.findOne({ + _id: params.id, + }); + + if (!conv) { + error(404, "Conversation not found"); + } + } + + const { value, mime } = await downloadFile(sha256, params.id); + + const b64Value = Buffer.from(value, "base64"); + return new Response(b64Value, { + headers: { + "Content-Type": mime ?? "application/octet-stream", + "Content-Security-Policy": + "default-src 'none'; script-src 'none'; style-src 'none'; sandbox;", + "Content-Disposition": `attachment; filename="${sha256.slice(0, 8)}.${ + mime ? mimeTypes.extension(mime) || "bin" : "bin" + }"`, + "Content-Length": b64Value.length.toString(), + "Accept-Range": "bytes", + }, + }); +}; diff --git a/src/routes/conversation/[id]/share/+server.ts b/src/routes/conversation/[id]/share/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..531adf2f852cf5f81b3239447bae2e8999220d42 --- /dev/null +++ b/src/routes/conversation/[id]/share/+server.ts @@ -0,0 +1,72 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import type { SharedConversation } from "$lib/types/SharedConversation"; +import { getShareUrl } from "$lib/utils/getShareUrl"; +import { hashConv } from "$lib/utils/hashConv"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { nanoid } from "nanoid"; + +export async function POST({ params, url, locals }) { + const conversation = await collections.conversations.findOne({ + _id: new ObjectId(params.id), + ...authCondition(locals), + }); + + if (!conversation) { + error(404, "Conversation not found"); + } + + const hash = await hashConv(conversation); + + const existingShare = await collections.sharedConversations.findOne({ hash }); + + if (existingShare) { + return new Response( + JSON.stringify({ + url: getShareUrl(url, existingShare._id), + }), + { headers: { "Content-Type": "application/json" } } + ); + } + + const shared: SharedConversation = { + _id: nanoid(7), + hash, + createdAt: new Date(), + updatedAt: new Date(), + rootMessageId: conversation.rootMessageId, + messages: conversation.messages, + title: conversation.title, + model: conversation.model, + embeddingModel: conversation.embeddingModel, + preprompt: conversation.preprompt, + assistantId: conversation.assistantId, + }; + + await collections.sharedConversations.insertOne(shared); + + // copy files from `${conversation._id}-` to `${shared._id}-` + const files = await collections.bucket + .find({ filename: { $regex: `${conversation._id}-` } }) + .toArray(); + + await Promise.all( + files.map(async (file) => { + const newFilename = file.filename.replace(`${conversation._id}-`, `${shared._id}-`); + // copy files from `${conversation._id}-` to `${shared._id}-` by downloading and reuploaidng + const downloadStream = collections.bucket.openDownloadStream(file._id); + const uploadStream = collections.bucket.openUploadStream(newFilename, { + metadata: { ...file.metadata, conversation: shared._id.toString() }, + }); + downloadStream.pipe(uploadStream); + }) + ); + + return new Response( + JSON.stringify({ + url: getShareUrl(url, shared._id), + }), + { headers: { "Content-Type": "application/json" } } + ); +} diff --git a/src/routes/conversation/[id]/stop-generating/+server.ts b/src/routes/conversation/[id]/stop-generating/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..a26c8100fb06ec4d4fc35bbfdef52ec840896308 --- /dev/null +++ b/src/routes/conversation/[id]/stop-generating/+server.ts @@ -0,0 +1,28 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +/** + * Ideally, we'd be able to detect the client-side abort, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 + */ +export async function POST({ params, locals }) { + const conversationId = new ObjectId(params.id); + + const conversation = await collections.conversations.findOne({ + _id: conversationId, + ...authCondition(locals), + }); + + if (!conversation) { + error(404, "Conversation not found"); + } + + await collections.abortedGenerations.updateOne( + { conversationId }, + { $set: { updatedAt: new Date() }, $setOnInsert: { createdAt: new Date() } }, + { upsert: true } + ); + + return new Response(); +} diff --git a/src/routes/healthcheck/+server.ts b/src/routes/healthcheck/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..edb40a0dd81f9f7014376f129630dd5f5ab4f095 --- /dev/null +++ b/src/routes/healthcheck/+server.ts @@ -0,0 +1,3 @@ +export async function GET() { + return new Response("OK", { status: 200 }); +} diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..b813adc3af999d0c68aba789a7240155029c0137 --- /dev/null +++ b/src/routes/login/+page.server.ts @@ -0,0 +1,27 @@ +import { redirect } from "@sveltejs/kit"; +import { getOIDCAuthorizationUrl } from "$lib/server/auth"; +import { base } from "$app/paths"; +import { env } from "$env/dynamic/private"; + +export const actions = { + async default({ url, locals, request }) { + const referer = request.headers.get("referer"); + let redirectURI = `${(referer ? new URL(referer) : url).origin}${base}/login/callback`; + + // TODO: Handle errors if provider is not responding + + if (url.searchParams.has("callback")) { + const callback = url.searchParams.get("callback") || redirectURI; + if (env.ALTERNATIVE_REDIRECT_URLS.includes(callback)) { + redirectURI = callback; + } + } + + const authorizationUrl = await getOIDCAuthorizationUrl( + { redirectURI }, + { sessionId: locals.sessionId } + ); + + redirect(303, authorizationUrl); + }, +}; diff --git a/src/routes/login/callback/+page.server.ts b/src/routes/login/callback/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..168b810b456b91d606edc1f2848814b3185c651e --- /dev/null +++ b/src/routes/login/callback/+page.server.ts @@ -0,0 +1,83 @@ +import { redirect, error } from "@sveltejs/kit"; +import { getOIDCUserData, validateAndParseCsrfToken } from "$lib/server/auth"; +import { z } from "zod"; +import { base } from "$app/paths"; +import { updateUser } from "./updateUser"; +import { env } from "$env/dynamic/private"; +import JSON5 from "json5"; + +const allowedUserEmails = z + .array(z.string().email()) + .optional() + .default([]) + .parse(JSON5.parse(env.ALLOWED_USER_EMAILS)); + +const allowedUserDomains = z + .array(z.string().regex(/\.\w+$/)) // Contains at least a dot + .optional() + .default([]) + .parse(JSON5.parse(env.ALLOWED_USER_DOMAINS)); + +export async function load({ url, locals, cookies, request, getClientAddress }) { + const { error: errorName, error_description: errorDescription } = z + .object({ + error: z.string().optional(), + error_description: z.string().optional(), + }) + .parse(Object.fromEntries(url.searchParams.entries())); + + if (errorName) { + error(400, errorName + (errorDescription ? ": " + errorDescription : "")); + } + + const { code, state, iss } = z + .object({ + code: z.string(), + state: z.string(), + iss: z.string().optional(), + }) + .parse(Object.fromEntries(url.searchParams.entries())); + + const csrfToken = Buffer.from(state, "base64").toString("utf-8"); + + const validatedToken = await validateAndParseCsrfToken(csrfToken, locals.sessionId); + + if (!validatedToken) { + error(403, "Invalid or expired CSRF token"); + } + + const { userData } = await getOIDCUserData( + { redirectURI: validatedToken.redirectUrl }, + code, + iss + ); + + // Filter by allowed user emails or domains + if (allowedUserEmails.length > 0 || allowedUserDomains.length > 0) { + if (!userData.email) { + error(403, "User not allowed: email not returned"); + } + const emailVerified = userData.email_verified ?? true; + if (!emailVerified) { + error(403, "User not allowed: email not verified"); + } + + const emailDomain = userData.email.split("@")[1]; + const isEmailAllowed = allowedUserEmails.includes(userData.email); + const isDomainAllowed = allowedUserDomains.includes(emailDomain); + + if (!isEmailAllowed && !isDomainAllowed) { + error(403, "User not allowed"); + } + } + + await updateUser({ + userData, + locals, + cookies, + userAgent: request.headers.get("user-agent") ?? undefined, + ip: getClientAddress(), + }); + + redirect(302, `${base}/`); +} diff --git a/src/routes/login/callback/updateUser.spec.ts b/src/routes/login/callback/updateUser.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..fefaf8b0f5a89418da5991d5d74908aff256d562 --- /dev/null +++ b/src/routes/login/callback/updateUser.spec.ts @@ -0,0 +1,151 @@ +import { assert, it, describe, afterEach, vi, expect } from "vitest"; +import type { Cookies } from "@sveltejs/kit"; +import { collections } from "$lib/server/database"; +import { updateUser } from "./updateUser"; +import { ObjectId } from "mongodb"; +import { DEFAULT_SETTINGS } from "$lib/types/Settings"; +import { defaultModel } from "$lib/server/models"; +import { findUser } from "$lib/server/auth"; +import { defaultEmbeddingModel } from "$lib/server/embeddingModels"; + +const userData = { + preferred_username: "new-username", + name: "name", + picture: "https://example.com/avatar.png", + sub: "1234567890", +}; +Object.freeze(userData); + +const locals = { + userId: "1234567890", + sessionId: "1234567890", +}; + +// @ts-expect-error SvelteKit cookies dumb mock +const cookiesMock: Cookies = { + set: vi.fn(), +}; + +const insertRandomUser = async () => { + const res = await collections.users.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + username: "base-username", + name: userData.name, + avatarUrl: userData.picture, + hfUserId: userData.sub, + }); + + return res.insertedId; +}; + +const insertRandomConversations = async (count: number) => { + const res = await collections.conversations.insertMany( + new Array(count).fill(0).map(() => ({ + _id: new ObjectId(), + title: "random title", + messages: [], + model: defaultModel.id, + embeddingModel: defaultEmbeddingModel.id, + createdAt: new Date(), + updatedAt: new Date(), + sessionId: locals.sessionId, + })) + ); + + return res.insertedIds; +}; + +describe("login", () => { + it("should update user if existing", async () => { + await insertRandomUser(); + + await updateUser({ userData, locals, cookies: cookiesMock }); + + const existingUser = await collections.users.findOne({ hfUserId: userData.sub }); + + assert.equal(existingUser?.name, userData.name); + + expect(cookiesMock.set).toBeCalledTimes(1); + }); + + it("should migrate pre-existing conversations for new user", async () => { + const insertedId = await insertRandomUser(); + + await insertRandomConversations(2); + + await updateUser({ userData, locals, cookies: cookiesMock }); + + const conversationCount = await collections.conversations.countDocuments({ + userId: insertedId, + sessionId: { $exists: false }, + }); + + assert.equal(conversationCount, 2); + + await collections.conversations.deleteMany({ userId: insertedId }); + }); + + it("should create default settings for new user", async () => { + await updateUser({ userData, locals, cookies: cookiesMock }); + + const user = await findUser(locals.sessionId); + + assert.exists(user); + + const settings = await collections.settings.findOne({ userId: user?._id }); + + expect(settings).toMatchObject({ + userId: user?._id, + updatedAt: expect.any(Date), + createdAt: expect.any(Date), + ethicsModalAcceptedAt: expect.any(Date), + ...DEFAULT_SETTINGS, + }); + + await collections.settings.deleteOne({ userId: user?._id }); + }); + + it("should migrate pre-existing settings for pre-existing user", async () => { + const { insertedId } = await collections.settings.insertOne({ + sessionId: locals.sessionId, + ethicsModalAcceptedAt: new Date(), + updatedAt: new Date(), + createdAt: new Date(), + ...DEFAULT_SETTINGS, + shareConversationsWithModelAuthors: false, + }); + + await updateUser({ userData, locals, cookies: cookiesMock }); + + const settings = await collections.settings.findOne({ + _id: insertedId, + sessionId: { $exists: false }, + }); + + assert.exists(settings); + + const user = await collections.users.findOne({ hfUserId: userData.sub }); + + expect(settings).toMatchObject({ + userId: user?._id, + updatedAt: expect.any(Date), + createdAt: expect.any(Date), + ethicsModalAcceptedAt: expect.any(Date), + ...DEFAULT_SETTINGS, + shareConversationsWithModelAuthors: false, + }); + + await collections.settings.deleteOne({ userId: user?._id }); + }); +}); + +afterEach(async () => { + await collections.users.deleteMany({ hfUserId: userData.sub }); + await collections.sessions.deleteMany({}); + + locals.userId = "1234567890"; + locals.sessionId = "1234567890"; + vi.clearAllMocks(); +}); diff --git a/src/routes/login/callback/updateUser.ts b/src/routes/login/callback/updateUser.ts new file mode 100644 index 0000000000000000000000000000000000000000..39993dfbfad4b1cb7580805a97d99961ef70446b --- /dev/null +++ b/src/routes/login/callback/updateUser.ts @@ -0,0 +1,199 @@ +import { refreshSessionCookie } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { DEFAULT_SETTINGS } from "$lib/types/Settings"; +import { z } from "zod"; +import type { UserinfoResponse } from "openid-client"; +import { error, type Cookies } from "@sveltejs/kit"; +import crypto from "crypto"; +import { sha256 } from "$lib/utils/sha256"; +import { addWeeks } from "date-fns"; +import { OIDConfig } from "$lib/server/auth"; +import { env } from "$env/dynamic/private"; +import { logger } from "$lib/server/logger"; + +export async function updateUser(params: { + userData: UserinfoResponse; + locals: App.Locals; + cookies: Cookies; + userAgent?: string; + ip?: string; +}) { + const { userData, locals, cookies, userAgent, ip } = params; + + // Microsoft Entra v1 tokens do not provide preferred_username, instead the username is provided in the upn + // claim. See https://learn.microsoft.com/en-us/entra/identity-platform/access-token-claims-reference + if (!userData.preferred_username && userData.upn) { + userData.preferred_username = userData.upn as string; + } + + const { + preferred_username: username, + name, + email, + picture: avatarUrl, + sub: hfUserId, + orgs, + } = z + .object({ + preferred_username: z.string().optional(), + name: z.string(), + picture: z.string().optional(), + sub: z.string(), + email: z.string().email().optional(), + orgs: z + .array( + z.object({ + sub: z.string(), + name: z.string(), + picture: z.string(), + preferred_username: z.string(), + isEnterprise: z.boolean(), + }) + ) + .optional(), + }) + .setKey(OIDConfig.NAME_CLAIM, z.string()) + .refine((data) => data.preferred_username || data.email, { + message: "Either preferred_username or email must be provided by the provider.", + }) + .transform((data) => ({ + ...data, + name: data[OIDConfig.NAME_CLAIM], + })) + .parse(userData) as { + preferred_username?: string; + email?: string; + picture?: string; + sub: string; + name: string; + orgs?: Array<{ + sub: string; + name: string; + picture: string; + preferred_username: string; + isEnterprise: boolean; + }>; + } & Record; + + // Dynamically access user data based on NAME_CLAIM from environment + // This approach allows us to adapt to different OIDC providers flexibly. + + logger.info( + { + login_username: username, + login_name: name, + login_email: email, + login_orgs: orgs?.map((el) => el.sub), + }, + "user login" + ); + // if using huggingface as auth provider, check orgs for earl access and amin rights + const isAdmin = (env.HF_ORG_ADMIN && orgs?.some((org) => org.sub === env.HF_ORG_ADMIN)) || false; + const isEarlyAccess = + (env.HF_ORG_EARLY_ACCESS && orgs?.some((org) => org.sub === env.HF_ORG_EARLY_ACCESS)) || false; + + logger.debug( + { + isAdmin, + isEarlyAccess, + hfUserId, + }, + `Updating user ${hfUserId}` + ); + + // check if user already exists + const existingUser = await collections.users.findOne({ hfUserId }); + let userId = existingUser?._id; + + // update session cookie on login + const previousSessionId = locals.sessionId; + const secretSessionId = crypto.randomUUID(); + const sessionId = await sha256(secretSessionId); + + if (await collections.sessions.findOne({ sessionId })) { + error(500, "Session ID collision"); + } + + locals.sessionId = sessionId; + + if (existingUser) { + // update existing user if any + await collections.users.updateOne( + { _id: existingUser._id }, + { $set: { username, name, avatarUrl, isAdmin, isEarlyAccess } } + ); + + // remove previous session if it exists and add new one + await collections.sessions.deleteOne({ sessionId: previousSessionId }); + await collections.sessions.insertOne({ + _id: new ObjectId(), + sessionId: locals.sessionId, + userId: existingUser._id, + createdAt: new Date(), + updatedAt: new Date(), + userAgent, + ip, + expiresAt: addWeeks(new Date(), 2), + }); + } else { + // user doesn't exist yet, create a new one + const { insertedId } = await collections.users.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + username, + name, + email, + avatarUrl, + hfUserId, + isAdmin, + isEarlyAccess, + }); + + userId = insertedId; + + await collections.sessions.insertOne({ + _id: new ObjectId(), + sessionId: locals.sessionId, + userId, + createdAt: new Date(), + updatedAt: new Date(), + userAgent, + ip, + expiresAt: addWeeks(new Date(), 2), + }); + + // move pre-existing settings to new user + const { matchedCount } = await collections.settings.updateOne( + { sessionId: previousSessionId }, + { + $set: { userId, updatedAt: new Date() }, + $unset: { sessionId: "" }, + } + ); + + if (!matchedCount) { + // if no settings found for user, create default settings + await collections.settings.insertOne({ + userId, + ethicsModalAcceptedAt: new Date(), + updatedAt: new Date(), + createdAt: new Date(), + ...DEFAULT_SETTINGS, + }); + } + } + + // refresh session cookie + refreshSessionCookie(cookies, secretSessionId); + + // migrate pre-existing conversations + await collections.conversations.updateMany( + { sessionId: previousSessionId }, + { + $set: { userId }, + $unset: { sessionId: "" }, + } + ); +} diff --git a/src/routes/logout/+page.server.ts b/src/routes/logout/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..935846a5da6c575a64408b6d6accd71c602e7a91 --- /dev/null +++ b/src/routes/logout/+page.server.ts @@ -0,0 +1,20 @@ +import { dev } from "$app/environment"; +import { base } from "$app/paths"; +import { env } from "$env/dynamic/private"; +import { collections } from "$lib/server/database"; +import { redirect } from "@sveltejs/kit"; + +export const actions = { + async default({ cookies, locals }) { + await collections.sessions.deleteOne({ sessionId: locals.sessionId }); + + cookies.delete(env.COOKIE_NAME, { + path: "/", + // So that it works inside the space's iframe + sameSite: dev || env.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none", + secure: !dev && !(env.ALLOW_INSECURE_COOKIES === "true"), + httpOnly: true, + }); + redirect(303, `${base}/`); + }, +}; diff --git a/src/routes/models/+page.svelte b/src/routes/models/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f75843948f2a2d4c4f4c8e700d0b9064520deb48 --- /dev/null +++ b/src/routes/models/+page.svelte @@ -0,0 +1,135 @@ + + + + {#if isHuggingChat} + HuggingChat - Models + + + + + {/if} + + +
+
+
+

Models

+ {#if isHuggingChat} + + + + {/if} +
+

All models available on {envPublic.PUBLIC_APP_NAME}

+
+ {#each data.models.filter((el) => !el.unlisted) as model, index (model.id)} +
+ +
+ {#if model.logoUrl} + {model.displayName} logo + {:else} + + {/if} + {#if model.tools} + + + + {/if} + {#if model.multimodal} + + + + {/if} + {#if model.reasoning} + + + + + + {/if} + {#if model.id === $settings.activeModel} + + Active + + {:else if index === 0} + + Default + + {/if} +
+ + {model.displayName} + + + {model.description || "-"} + +
+ {/each} +
+
+
diff --git a/src/routes/models/[...model]/+page.server.ts b/src/routes/models/[...model]/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e385ece615f53a495c601763102a0d8edc212e4 --- /dev/null +++ b/src/routes/models/[...model]/+page.server.ts @@ -0,0 +1,39 @@ +import { base } from "$app/paths"; +import { authCondition } from "$lib/server/auth.js"; +import { collections } from "$lib/server/database"; +import { models } from "$lib/server/models"; +import { redirect } from "@sveltejs/kit"; + +export async function load({ params, locals, parent }) { + const model = models.find(({ id }) => id === params.model); + const data = await parent(); + + if (!model || model.unlisted) { + redirect(302, `${base}/`); + } + + if (locals.user?._id ?? locals.sessionId) { + await collections.settings.updateOne( + authCondition(locals), + { + $set: { + activeModel: model.id, + updatedAt: new Date(), + }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { + upsert: true, + } + ); + } + + return { + settings: { + ...data.settings, + activeModel: model.id, + }, + }; +} diff --git a/src/routes/models/[...model]/+page.svelte b/src/routes/models/[...model]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4a71e63406a1bb76bbc30dc015e8860a83584bbe --- /dev/null +++ b/src/routes/models/[...model]/+page.svelte @@ -0,0 +1,86 @@ + + + + + + + + + + + + createConversation(ev.detail)} + {loading} + currentModel={findCurrentModel([...data.models, ...data.oldModels], modelId)} + models={data.models} + bind:files +/> diff --git a/src/routes/models/[...model]/thumbnail.png/+server.ts b/src/routes/models/[...model]/thumbnail.png/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..092b94dfd769e1da26e3b510ff96997841d1a91c --- /dev/null +++ b/src/routes/models/[...model]/thumbnail.png/+server.ts @@ -0,0 +1,57 @@ +import ModelThumbnail from "./ModelThumbnail.svelte"; +import { redirect, type RequestHandler } from "@sveltejs/kit"; + +import { Resvg } from "@resvg/resvg-js"; +import satori from "satori"; +import { html } from "satori-html"; + +import InterRegular from "$lib/server/fonts/Inter-Regular.ttf"; +import InterBold from "$lib/server/fonts/Inter-Bold.ttf"; +import { base } from "$app/paths"; +import { models } from "$lib/server/models"; +import { render } from "svelte/server"; + +export const GET: RequestHandler = (async ({ params }) => { + const model = models.find(({ id }) => id === params.model); + + if (!model || model.unlisted) { + redirect(302, `${base}/`); + } + const renderedComponent = render(ModelThumbnail, { + props: { + name: model.name, + logoUrl: model.logoUrl, + }, + }); + + const reactLike = html("" + renderedComponent.body); + + const svg = await satori(reactLike, { + width: 1200, + height: 648, + fonts: [ + { + name: "Inter", + data: InterRegular as unknown as ArrayBuffer, + weight: 500, + }, + { + name: "Inter", + data: InterBold as unknown as ArrayBuffer, + weight: 700, + }, + ], + }); + + const png = new Resvg(svg, { + fitTo: { mode: "original" }, + }) + .render() + .asPng(); + + return new Response(png, { + headers: { + "Content-Type": "image/png", + }, + }); +}) satisfies RequestHandler; diff --git a/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte b/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b67ac049162c70b1e6c0e4fa21776e8c83458bef --- /dev/null +++ b/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte @@ -0,0 +1,43 @@ + + +
+
+ {#if logoUrl} + avatar + {/if} +

+ {name} +

+
+ +
+ Try it now + {#if isHuggingChat} + on + {/if} + + {#if isHuggingChat} +
+ + HuggingChat +
+ {/if} +
+
diff --git a/src/routes/privacy/+page.svelte b/src/routes/privacy/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f50fa73a6017b02725a3f941ee796c56ae8d8b6d --- /dev/null +++ b/src/routes/privacy/+page.svelte @@ -0,0 +1,11 @@ + + +
+
+ + {@html marked(privacy, { gfm: true })} +
+
diff --git a/src/routes/r/[id]/+page.ts b/src/routes/r/[id]/+page.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc653ca5b36525989ab30c3f164d7805868dc677 --- /dev/null +++ b/src/routes/r/[id]/+page.ts @@ -0,0 +1,7 @@ +import { redirect, type LoadEvent } from "@sveltejs/kit"; + +export const load = async ({ params, url }: LoadEvent) => { + const leafId = url.searchParams.get("leafId"); + + redirect(302, "../conversation/" + params.id + `?leafId=${leafId}`); +}; diff --git a/src/routes/settings/(nav)/+layout.svelte b/src/routes/settings/(nav)/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2350f901c80503facc5d0a39c704ecb881923bcd --- /dev/null +++ b/src/routes/settings/(nav)/+layout.svelte @@ -0,0 +1,305 @@ + + +
+
+ {#if showContent && browser} + + {/if} +

+ Settings +

+ +
+ {#if !(showContent && browser && !isDesktop(window))} +
+ +

+ Models +

+ + {#each data.models.filter((el) => !el.unlisted) as model} + + {/each} + {#if data.enableAssistants} +

+ Assistants +

+ +

+ My Assistants +

+ + {#each data.assistants.filter((assistant) => assistant.createdByMe) as assistant} + + {/each} + {#if !data.loginEnabled || (data.loginEnabled && !!data.user)} + + {/if} + + +

+ Other Assistants +

+ + {#each data.assistants.filter((assistant) => !assistant.createdByMe) as assistant} +
+ +
+ +
+
+ {/each} + + {/if} + +
+ +
+ {/if} + {#if showContent} +
+ {@render children?.()} +
+ {/if} +
diff --git a/src/routes/settings/(nav)/+layout.ts b/src/routes/settings/(nav)/+layout.ts new file mode 100644 index 0000000000000000000000000000000000000000..a3d15781a772c9c833d435893cc10dc9999f63c2 --- /dev/null +++ b/src/routes/settings/(nav)/+layout.ts @@ -0,0 +1 @@ +export const ssr = false; diff --git a/src/routes/settings/(nav)/+page.svelte b/src/routes/settings/(nav)/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/routes/settings/(nav)/+server.ts b/src/routes/settings/(nav)/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ca6177b213e9163ef70c012189253e93d1d8e4b --- /dev/null +++ b/src/routes/settings/(nav)/+server.ts @@ -0,0 +1,61 @@ +import { collections } from "$lib/server/database"; +import { z } from "zod"; +import { authCondition } from "$lib/server/auth"; +import { DEFAULT_SETTINGS, type SettingsEditable } from "$lib/types/Settings"; +import { toolFromConfigs } from "$lib/server/tools/index.js"; +import { ObjectId } from "mongodb"; + +export async function POST({ request, locals }) { + const body = await request.json(); + + const { ethicsModalAccepted, ...settings } = z + .object({ + shareConversationsWithModelAuthors: z + .boolean() + .default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors), + hideEmojiOnSidebar: z.boolean().default(DEFAULT_SETTINGS.hideEmojiOnSidebar), + ethicsModalAccepted: z.boolean().optional(), + activeModel: z.string().default(DEFAULT_SETTINGS.activeModel), + customPrompts: z.record(z.string()).default({}), + tools: z.array(z.string()).optional(), + disableStream: z.boolean().default(false), + directPaste: z.boolean().default(false), + }) + .parse(body) satisfies SettingsEditable; + + // make sure all tools exist + // either in db or in config + if (settings.tools) { + const newTools = [ + ...(await collections.tools + .find({ _id: { $in: settings.tools.map((toolId) => new ObjectId(toolId)) } }) + .project({ _id: 1 }) + .toArray() + .then((tools) => tools.map((tool) => tool._id.toString()))), + ...toolFromConfigs + .filter((el) => (settings?.tools ?? []).includes(el._id.toString())) + .map((el) => el._id.toString()), + ]; + + settings.tools = newTools; + } + + await collections.settings.updateOne( + authCondition(locals), + { + $set: { + ...settings, + ...(ethicsModalAccepted && { ethicsModalAcceptedAt: new Date() }), + updatedAt: new Date(), + }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { + upsert: true, + } + ); + // return ok response + return new Response(); +} diff --git a/src/routes/settings/(nav)/[...model]/+page.svelte b/src/routes/settings/(nav)/[...model]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f96ed4b701293121e5260939b4da697c0fc7716a --- /dev/null +++ b/src/routes/settings/(nav)/[...model]/+page.svelte @@ -0,0 +1,106 @@ + + +
+
+

+ {page.params.model} +

+ + {#if model.description} +

+ {model.description} +

+ {/if} +
+ +
+ + +
+ Copy direct link to model +
+
+
+ + + +
+
+

System Prompt

+ {#if hasCustomPreprompt} + + {/if} +
+ + {#if model.tokenizer && $settings.customPrompts[page.params.model]} + + {/if} +
+
diff --git a/src/routes/settings/(nav)/[...model]/+page.ts b/src/routes/settings/(nav)/[...model]/+page.ts new file mode 100644 index 0000000000000000000000000000000000000000..57f70b7da73166bdf3cebdbc4560225613c65624 --- /dev/null +++ b/src/routes/settings/(nav)/[...model]/+page.ts @@ -0,0 +1,14 @@ +import { base } from "$app/paths"; +import { redirect } from "@sveltejs/kit"; + +export async function load({ parent, params }) { + const data = await parent(); + + const model = data.models.find((m: { id: string }) => m.id === params.model); + + if (!model || model.unlisted) { + redirect(302, `${base}/settings`); + } + + return data; +} diff --git a/src/routes/settings/(nav)/application/+page.svelte b/src/routes/settings/(nav)/application/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..590892fbf0fdc4657dee6e671e228b59b7e60479 --- /dev/null +++ b/src/routes/settings/(nav)/application/+page.svelte @@ -0,0 +1,105 @@ + + +
+

Application Settings

+ {#if !!envPublic.PUBLIC_COMMIT_SHA} + + {/if} +
+ {#if envPublic.PUBLIC_APP_DATA_SHARING === "1"} + + +

+ Sharing your data will help improve the training data and make open models better over time. +

+ {/if} + + + + + + +
+ Share your feedback on HuggingChat + +
+
+
diff --git a/src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte b/src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..78944e007a4b61f02c4def9f28bbee56e5752161 --- /dev/null +++ b/src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte @@ -0,0 +1,415 @@ + + +{#if displayReportModal} + (displayReportModal = false)} + reportUrl={`${base}/api/assistant/${assistant?._id}/report`} + /> +{/if} +
+
+
+ {#if assistant?.avatar} + Avatar + {:else} +
+ {assistant?.name[0]} +
+ {/if} +
+ +
+
+

+ {assistant?.name} +

+ + {#if hasRag} + + + + {/if} + public +
+ + {#if assistant?.description} +

+ {assistant.description} +

+ {/if} + +

+ Model: {assistant?.modelId} + Created by + + {assistant?.createdByName} + +

+
+
+ +
+ {#if assistant?.createdByMe} + Edit + +
{ + fetch(`${base}/api/assistant/${assistant?._id}`, { + method: "DELETE", + }).then((r) => { + if (r.ok) { + goto(`${base}/settings/assistants`, { invalidateAll: true }); + } else { + console.error(r); + $error = r.statusText; + } + }); + }} + > + +
+ {:else} +
{ + fetch(`${base}/api/assistant/${assistant?._id}/subscribe`, { + method: "DELETE", + }).then((r) => { + if (r.ok) { + goto(`${base}/settings/assistants`, { invalidateAll: true }); + } else { + console.error(r); + $error = r.statusText; + } + }); + }} + > + +
+ + {#if !assistant?.reported} + + {:else} + + {/if} + {/if} + {#if data?.user?.isAdmin} + {assistant?.review?.toLocaleUpperCase()} + + {#if !assistant?.createdByMe} +
{ + fetch(`${base}/api/assistant/${assistant?._id}`, { + method: "DELETE", + }).then((r) => { + if (r.ok) { + goto(`${base}/settings/assistants`, { invalidateAll: true }); + } else { + console.error(r); + $error = r.statusText; + } + }); + }} + > + +
+ {/if} + {#if assistant?.review === ReviewStatus.PRIVATE} +
setFeatured(ReviewStatus.APPROVED)}> + +
+ {/if} + {#if assistant?.review === ReviewStatus.PENDING} +
setFeatured(ReviewStatus.APPROVED)}> + +
+
setFeatured(ReviewStatus.DENIED)}> + +
+ {/if} + {#if assistant?.review === ReviewStatus.APPROVED || assistant?.review === ReviewStatus.DENIED} +
setFeatured(ReviewStatus.PRIVATE)}> + +
+ {/if} + {/if} + {#if assistant?.createdByMe && assistant?.review === ReviewStatus.PRIVATE} +
{ + const confirmed = confirm( + "Are you sure you want to request this assistant to be featured? Make sure you have tried the assistant and that it works as expected. " + ); + + if (!confirmed) { + return; + } + + setFeatured(ReviewStatus.PENDING); + }} + > + +
+ {/if} +
+
+
+ +
+

Direct URL

+ +

Share this link for people to use your assistant.

+ +
+ + +
+ Copy +
+
+
+
+ + +
+

System Instructions

+
+ {#if assistant?.dynamicPrompt} + {#each prepromptTags as tag} + {#if (tag.startsWith("{{") && tag.endsWith("}}") && (tag.includes("get=") || tag.includes("post=") || tag.includes("url="))) || tag.includes("today")} + {@const url = tag.match(/(?:get|post|url)=(.*?)}}/)?.[1] ?? ""} + + {tag} + {:else} + {tag} + {/if} + {/each} + {:else} + {assistant?.preprompt} + {/if} +
+ + {#if assistant?.tools?.length} +
+
+ + + +

Tools

+
+

+ This Assistant has access to the following tools: +

+
    + {#each assistant.tools as tool} + + {/each} +
+
+ {/if} + {#if hasRag} +
+
+ + + +

Internet Access

+
+ {#if assistant?.rag?.allowAllDomains} +

+ This Assistant uses Web Search to find information on Internet. +

+ {:else if !!assistant?.rag?.allowedDomains && assistant?.rag?.allowedDomains.length} +

+ This Assistant can use Web Search on the following domains: +

+
    + {#each assistant?.rag?.allowedDomains as domain} +
  • + {domain} +
  • + {/each} +
+ {:else if !!assistant?.rag?.allowedLinks && assistant?.rag?.allowedLinks.length} +

This Assistant can browse the following links:

+
    + {#each assistant?.rag?.allowedLinks as link} +
  • + {link} +
  • + {/each} +
+ {/if} + {#if assistant?.dynamicPrompt} +

+ This Assistant has dynamic prompts enabled and can make requests to external services. +

+ {/if} +
+ {/if} +
+
diff --git a/src/routes/settings/(nav)/assistants/[assistantId]/+page.ts b/src/routes/settings/(nav)/assistants/[assistantId]/+page.ts new file mode 100644 index 0000000000000000000000000000000000000000..152aac276d8dc8c43a87f27c7316dc9f09ea84d5 --- /dev/null +++ b/src/routes/settings/(nav)/assistants/[assistantId]/+page.ts @@ -0,0 +1,14 @@ +import { base } from "$app/paths"; +import { redirect } from "@sveltejs/kit"; + +export async function load({ parent, params }) { + const data = await parent(); + + const assistant = data.settings.assistants.find((id) => id === params.assistantId); + + if (!assistant) { + redirect(302, `${base}/assistant/${params.assistantId}`); + } + + return data; +} diff --git a/src/routes/settings/(nav)/assistants/[assistantId]/ReportModal.svelte b/src/routes/settings/(nav)/assistants/[assistantId]/ReportModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3ce4766044d118febcf6df11da41f12656f36e2c --- /dev/null +++ b/src/routes/settings/(nav)/assistants/[assistantId]/ReportModal.svelte @@ -0,0 +1,62 @@ + + + +
{ + fetch(`${reportUrl}`, { + method: "POST", + body: JSON.stringify({ reason }), + }).then(() => { + dispatch("close"); + invalidateAll(); + }); + }} + class="w-full min-w-64 p-4" + > + Report content + +

+ Please provide a brief description of why you are reporting this content. +

+ + + +
+ + + +
+
+
diff --git a/src/routes/settings/(nav)/assistants/[assistantId]/avatar.jpg/+server.ts b/src/routes/settings/(nav)/assistants/[assistantId]/avatar.jpg/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..7258dc16673a67d20689da8c970164a12c7d0f71 --- /dev/null +++ b/src/routes/settings/(nav)/assistants/[assistantId]/avatar.jpg/+server.ts @@ -0,0 +1,44 @@ +import { collections } from "$lib/server/database"; +import { error, type RequestHandler } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +export const GET: RequestHandler = async ({ params }) => { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(params.assistantId), + }); + + if (!assistant) { + error(404, "No assistant found"); + } + + if (!assistant.avatar) { + error(404, "No avatar found"); + } + + const fileId = collections.bucket.find({ filename: assistant._id.toString() }); + + const content = await fileId.next().then(async (file) => { + if (!file?._id) { + error(404, "Avatar not found"); + } + + const fileStream = collections.bucket.openDownloadStream(file?._id); + + const fileBuffer = await new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + fileStream.on("data", (chunk) => chunks.push(chunk)); + fileStream.on("error", reject); + fileStream.on("end", () => resolve(Buffer.concat(chunks))); + }); + + return fileBuffer; + }); + + return new Response(content, { + headers: { + "Content-Type": "image/jpeg", + "Content-Security-Policy": + "default-src 'none'; script-src 'none'; style-src 'none'; sandbox;", + }, + }); +}; diff --git a/src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.svelte b/src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1b0ac21b3a129c79602ac268469c9fae4a7b4144 --- /dev/null +++ b/src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/routes/settings/(nav)/assistants/new/+page.svelte b/src/routes/settings/(nav)/assistants/new/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b6f29a85c732f80fd50d2cde4062aef1dccc82d4 --- /dev/null +++ b/src/routes/settings/(nav)/assistants/new/+page.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/routes/settings/+layout.server.ts b/src/routes/settings/+layout.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..17119bc8c8c455e6522335070b936f60bd79e029 --- /dev/null +++ b/src/routes/settings/+layout.server.ts @@ -0,0 +1,25 @@ +import { collections } from "$lib/server/database"; +import type { LayoutServerLoad } from "./$types"; +import type { Report } from "$lib/types/Report"; + +export const load = (async ({ locals, parent }) => { + const { assistants } = await parent(); + + let reportsByUser: string[] = []; + const createdBy = locals.user?._id ?? locals.sessionId; + if (createdBy) { + const reports = await collections.reports + .find< + Pick + >({ createdBy, object: "assistant" }, { projection: { _id: 0, contentId: 1 } }) + .toArray(); + reportsByUser = reports.map((r) => r.contentId.toString()); + } + + return { + assistants: (await assistants).map((el) => ({ + ...el, + reported: reportsByUser.includes(el._id), + })), + }; +}) satisfies LayoutServerLoad; diff --git a/src/routes/settings/+layout.svelte b/src/routes/settings/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..55907be782696d5933afdbaee970392e4c06364c --- /dev/null +++ b/src/routes/settings/+layout.svelte @@ -0,0 +1,39 @@ + + + goto(previousPage)} + width="h-[95dvh] w-[90dvw] pb-0 overflow-hidden rounded-2xl bg-white shadow-2xl outline-none sm:h-[95dvh] xl:w-[1200px] 2xl:h-[75dvh]" +> + {@render children?.()} + {#if $settings.recentlySaved} +
+ + Saved +
+ {/if} +
diff --git a/src/routes/tools/+layout.svelte b/src/routes/tools/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fd0114462513343794bf8c121cac2598964f897e --- /dev/null +++ b/src/routes/tools/+layout.svelte @@ -0,0 +1,28 @@ + + + + {#if isHuggingChat} + HuggingChat - Tools + + + + + + {/if} + + +{@render children?.()} diff --git a/src/routes/tools/+layout.ts b/src/routes/tools/+layout.ts new file mode 100644 index 0000000000000000000000000000000000000000..40ff7f32b065b693a2f5e1ccfdfd64480b20db4a --- /dev/null +++ b/src/routes/tools/+layout.ts @@ -0,0 +1,12 @@ +import { base } from "$app/paths"; +import { redirect } from "@sveltejs/kit"; + +export async function load({ parent }) { + const { enableCommunityTools } = await parent(); + + if (enableCommunityTools) { + return {}; + } + + redirect(302, `${base}/`); +} diff --git a/src/routes/tools/+page.server.ts b/src/routes/tools/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7b0ee3e016be037f96b3c5ed8ad95e9683c6b7c --- /dev/null +++ b/src/routes/tools/+page.server.ts @@ -0,0 +1,96 @@ +import { env } from "$env/dynamic/private"; +import { authCondition } from "$lib/server/auth.js"; +import { collections } from "$lib/server/database.js"; +import { toolFromConfigs } from "$lib/server/tools/index.js"; +import { SortKey } from "$lib/types/Assistant.js"; +import { ReviewStatus } from "$lib/types/Review"; +import type { CommunityToolDB } from "$lib/types/Tool.js"; +import type { User } from "$lib/types/User.js"; +import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens.js"; +import { error } from "@sveltejs/kit"; +import { ObjectId, type Filter } from "mongodb"; + +const NUM_PER_PAGE = 16; + +export const load = async ({ url, locals }) => { + if (env.COMMUNITY_TOOLS !== "true") { + error(403, "Community tools are not enabled"); + } + + const username = url.searchParams.get("user"); + const query = url.searchParams.get("q")?.trim() ?? null; + + const pageIndex = parseInt(url.searchParams.get("p") ?? "0"); + const sort = url.searchParams.get("sort")?.trim() ?? SortKey.TRENDING; + const createdByCurrentUser = locals.user?.username && locals.user.username === username; + const activeOnly = url.searchParams.get("active") === "true"; + const showUnfeatured = url.searchParams.get("showUnfeatured") === "true"; + + let user: Pick | null = null; + if (username) { + user = await collections.users.findOne>( + { username }, + { projection: { _id: 1 } } + ); + if (!user) { + error(404, `User "${username}" doesn't exist`); + } + } + + const settings = await collections.settings.findOne(authCondition(locals)); + + if (!settings && activeOnly) { + error(404, "No user settings found"); + } + + const queryTokens = !!query && generateQueryTokens(query); + + const filter: Filter = { + ...(!createdByCurrentUser && + !activeOnly && + !(locals.user?.isAdmin && showUnfeatured) && { review: ReviewStatus.APPROVED }), + ...(user && { createdById: user._id }), + ...(queryTokens && { searchTokens: { $all: queryTokens } }), + ...(activeOnly && { + _id: { + $in: (settings?.tools ?? []).map((key) => { + return new ObjectId(key); + }), + }, + }), + }; + + const communityTools = await collections.tools + .find(filter) + .skip(NUM_PER_PAGE * pageIndex) + .sort({ + ...(sort === SortKey.TRENDING && { last24HoursUseCount: -1 }), + useCount: -1, + }) + .limit(NUM_PER_PAGE) + .toArray(); + + const configTools = toolFromConfigs + .filter((tool) => !tool?.isHidden) + .filter((tool) => { + if (queryTokens) { + return generateSearchTokens(tool.displayName).some((token) => + queryTokens.some((queryToken) => queryToken.test(token)) + ); + } + return true; + }); + + const tools = [...(pageIndex == 0 && !username ? configTools : []), ...communityTools]; + + const numTotalItems = (await collections.tools.countDocuments(filter)) + toolFromConfigs.length; + + return { + tools: JSON.parse(JSON.stringify(tools)) as CommunityToolDB[], + numTotalItems, + numItemsPerPage: NUM_PER_PAGE, + query, + sort, + showUnfeatured, + }; +}; diff --git a/src/routes/tools/+page.svelte b/src/routes/tools/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..920fb10dd7944c7ac6e7e028e9470d661ded14ad --- /dev/null +++ b/src/routes/tools/+page.svelte @@ -0,0 +1,346 @@ + + +
+
+
+

Tools

+ {#if isHuggingChat} +
+ beta +
+ + + + {/if} +
+

Popular tools made by the community

+

+ This feature is experimental. Consider + sharing your feedback with us! +

+
+ {#if data.user?.isAdmin} + + {/if} + {#if page.data.loginRequired && !data.user} + + {:else} + + Create new tool + + {/if} +
+ +
+ {#if toolsCreator && !createdByMe} +
+ {toolsCreator}'s tools + +
+ {#if isHuggingChat} + View {toolsCreator} + on HF + {/if} + {:else} + + + Active ({page.data.settings?.tools?.length}) + + + + Community + + {#if data.user?.username} + {data.user.username} + + {/if} + {/if} +
+ + filterOnName(e.currentTarget.value)} + bind:this={filterInputEl} + maxlength="150" + type="search" + aria-label="Filter tools by name" + /> +
+ +
+ + {#if !currentModelSupportTools} +
+ You are currently not using a model that supports tools. Activate one + here. +
+ {/if} + +
+ {#each tools as tool} + {@const isActive = (page.data.settings?.tools ?? []).includes(tool._id.toString())} + {@const isOfficial = !tool.createdByName} +
goto(`${base}/tools/${tool._id.toString()}`)} + onkeydown={(e) => e.key === "Enter" && goto(`${base}/tools/${tool._id.toString()}`)} + role="button" + tabindex="0" + class="relative flex flex-row items-center gap-4 overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 text-center shadow hover:bg-gray-50 hover:shadow-inner dark:bg-gray-950/20 dark:hover:bg-gray-950/40 max-sm:px-4 sm:h-24 {!( + tool.review === ReviewStatus.APPROVED + ) && !isOfficial + ? ' border-red-500/30' + : 'dark:border-gray-800/70'}" + class:!border-blue-600={isActive} + > + {#key tool.color + tool.icon} + + {/key} +
+ + + {tool.displayName} + + {#if isActive} + Active + {/if} + + + {tool.baseUrl ?? "Internal tool"} + + +

+ {tool.description} +

+ + {#if !isOfficial} +

+ Added by { + e.stopPropagation(); + bubble("click"); + }} + > + {tool.createdByName} + + + {#if tool.useCount === 1} + 1 run + {:else} + {tool.useCount} runs + {/if} +

+ {:else} +

+ HuggingChat official tool +

+ {/if} +
+
+ {:else} + {#if activeOnly} + You don't have any active tools. + {:else} + No tools found + {/if} + {/each} +
+ + +
+
diff --git a/src/routes/tools/ToolEdit.svelte b/src/routes/tools/ToolEdit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..754f5c711e90b00e4acdf97e083f80979ce78cbd --- /dev/null +++ b/src/routes/tools/ToolEdit.svelte @@ -0,0 +1,627 @@ + + +
{ + e.preventDefault(); + formLoading = true; + errors = []; + + try { + const body = JSON.stringify(editableTool); + let response: Response; + + if (page.params.toolId) { + response = await fetch(`${base}/api/tools/${page.params.toolId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body, + }); + } else { + response = await fetch(`${base}/api/tools`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body, + }); + } + + if (response.ok) { + const { toolId } = await response.json(); + goto(`${base}/tools/${toolId}`, { invalidateAll: true }); + } else if (response.status === 400) { + const data = await response.json(); + errors = data.errors; + } else { + $errorStore = response.statusText; + } + } catch (e) { + $errorStore = (e as Error).message; + } finally { + formLoading = false; + } + }} +> + {#if tool} +

+ {readonly ? "View" : "Edit"} Tool: {tool.displayName} +

+ {#if !readonly} +

+ Modifying an existing tool will propagate the changes to all users. +

+ {/if} + {:else} +

Create new tool

+

+ Create and share your own tools. All tools are public +

+ {/if} + +
+
+
+ + +
+
+ {#key editableTool.color + editableTool.icon} + + {/key} +
+ + + + +
+ + + + +

+ Tools allows models that support them to use external application directly via function + calling. Tools must use Hugging Face Gradio Spaces as we detect the input and output types + automatically from the Gradio API. For GPU intensive tool consider using a ZeroGPU Space. +

+
+
+
+
+

Functions

+ {#if editableTool.baseUrl} +

Choose functions that can be called in your tool.

+ {:else} +

Start by specifying a Hugging Face Space URL.

+ {/if} + + {#if editableTool.baseUrl} + {#await getGradioApi(spaceUrl)} +

Loading...

+ {:then api} +
+ {#each Object.keys(api["named_endpoints"] ?? {}) as name} + + {/each} +
+ + {#if editableTool.endpoint && api["named_endpoints"][editableTool.endpoint] && !APIloading} + {@const endpoint = api["named_endpoints"][editableTool.endpoint]} +
+
+
+ {editableTool.endpoint} + + +
+ +
+

Arguments

+

+ Choose parameters that can be passed to your tool. +

+
+ +

+ {getError("inputs")} +

+ + {#each editableTool.inputs as input, inputIdx} + {@const parameter = endpoint.parameters.find( + (parameter) => parameter.parameter_name === input.name + )} + +
+
+ {input.name} + {parameter?.python_type.type} + {#if parameter?.description} +

+ {parameter.description} +

+ {/if} +
+ + + +
+
+
+ + {#if input.paramType === "required" || input.paramType === "optional"} + + {/if} + {#if input.paramType === "optional" || input.paramType === "fixed"} + {@const isOptional = input.paramType === "optional"} +
+
+ {isOptional ? "Default value" : "Value"} +

+ {#if isOptional} + The tool will use this value by default but the model can specify a + different one. + {:else} + The tool will use this value and it cannot be changed. + {/if} +

+
+ {#if input.paramType === "optional"} + + {:else} + + {/if} +
+ {/if} + {#if input.type === "file"} + + {/if} + +
+ {/each} + +
+
+

Output options

+

+ Choose what value your tool will return and how +

+
+ + + + +
+
+
+ {:else if APIloading} +

Loading API...

+ {:else if !api["named_endpoints"]} +

+ No endpoints found in this space. Try another one. +

+ {/if} + {:catch error} +

{error}

+ {/await} + {/if} +
+
+ + {#if !readonly} + + {/if} +
+
+
+
diff --git a/src/routes/tools/ToolInputComponent.svelte b/src/routes/tools/ToolInputComponent.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c643ba10a7b84f4e97d76c5efceb5ccc99e01257 --- /dev/null +++ b/src/routes/tools/ToolInputComponent.svelte @@ -0,0 +1,100 @@ + + +{#if type === "str" && typeof innerValue === "string"} + +{:else if type === "int" && typeof innerValue === "number"} + { + const value = e.currentTarget.value; + if (value === "" || isNaN(parseInt(value))) { + innerValue = previousValue; + e.currentTarget.value = previousValue.toString(); + return; + } else { + innerValue = parseFloat(value); + previousValue = innerValue; + } + }} + value={innerValue} + /> +{:else if type === "float" && typeof innerValue === "number"} + { + const value = e.currentTarget.value; + if (value === "" || isNaN(parseFloat(value))) { + innerValue = previousValue; + e.currentTarget.value = previousValue.toString(); + return; + } else { + innerValue = parseFloat(value); + previousValue = innerValue; + } + }} + value={innerValue} + /> +{:else if type === "bool" && typeof innerValue === "boolean"} + + +{:else if type.startsWith("Literal[") && typeof innerValue === "string"} + {@const options = type + .slice(8, -1) + .split(",") + .map((option) => option.trim().replaceAll("'", ""))} + +{:else} + {innerValue}-{typeof innerValue} +{/if} diff --git a/src/routes/tools/[toolId]/+layout.server.ts b/src/routes/tools/[toolId]/+layout.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..3fa4ed3050316bce6f13ae39eb798f88c036617d --- /dev/null +++ b/src/routes/tools/[toolId]/+layout.server.ts @@ -0,0 +1,46 @@ +import { base } from "$app/paths"; +import { collections } from "$lib/server/database.js"; +import { toolFromConfigs } from "$lib/server/tools/index.js"; +import { ReviewStatus } from "$lib/types/Review.js"; +import { redirect } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +export const load = async ({ params, locals }) => { + const tool = await collections.tools.findOne({ _id: new ObjectId(params.toolId) }); + + if (!tool) { + const tool = toolFromConfigs.find((el) => el._id.toString() === params.toolId); + if (!tool) { + redirect(302, `${base}/tools`); + } + return { + tool: { + ...tool, + _id: tool._id.toString(), + call: undefined, + createdById: null, + createdByName: null, + createdByMe: false, + reported: false, + review: ReviewStatus.APPROVED, + }, + }; + } + + const reported = await collections.reports.findOne({ + contentId: tool._id, + object: "tool", + }); + + return { + tool: { + ...tool, + _id: tool._id.toString(), + call: undefined, + createdById: tool.createdById.toString(), + createdByMe: + tool.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), + reported: !!reported, + }, + }; +}; diff --git a/src/routes/tools/[toolId]/+page.svelte b/src/routes/tools/[toolId]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..73d060ab6a3e995996634b03dc17496a88a3b0ea --- /dev/null +++ b/src/routes/tools/[toolId]/+page.svelte @@ -0,0 +1,334 @@ + + +{#if displayReportModal} + (displayReportModal = false)} + reportUrl={`${base}/api/tools/${data.tool?._id}/report`} + /> +{/if} + + goto(previousPage)} width="min-w-xl" closeButton> +
+
+
+
+ {#key data.tool.color + data.tool.icon} + + {/key} +
+ +
+
+

+ {data.tool.displayName} +

+ public +
+ + {#if data.tool?.baseUrl} + {#if data.tool.baseUrl.startsWith("https://")} +

+ {data.tool.baseUrl} +

+ {:else} + + {data.tool.baseUrl} + + {/if} + {/if} + + {#if data.tool.type === "community"} +

+ Added by + + {data.tool?.createdByName} + + + {#if data.tool.useCount === 1} + 1 run + {:else} + {data.tool.useCount} runs + {/if} +

+ {/if} + +
+
+ {#if currentModelSupportTools} + + {:else} + + {/if} +
+ {#if data.tool?.createdByMe} + Edit + +
{ + fetch(`${base}/api/tools/${data.tool?._id}`, { + method: "DELETE", + }).then((r) => { + if (r.ok) { + goto(`${base}/tools`, { invalidateAll: true }); + } else { + console.error(r); + $error = r.statusText; + } + }); + }} + > + +
+ {:else if !!data.tool?.baseUrl} + + View spec + + + {#if !data.tool?.reported} + + {:else} + + {/if} + {/if} + {#if data?.user?.isAdmin} + {data.tool?.review?.toLocaleUpperCase()} + + {#if !data.tool?.createdByMe} +
{ + fetch(`${base}/api/tools/${data.tool?._id}`, { + method: "DELETE", + }).then((r) => { + if (r.ok) { + goto(`${base}/tools`, { invalidateAll: true }); + } else { + console.error(r); + $error = r.statusText; + } + }); + }} + > + +
+ {/if} + {#if data.tool?.review === ReviewStatus.PRIVATE} +
setFeatured(ReviewStatus.APPROVED)}> + +
+ {/if} + {#if data.tool?.review === ReviewStatus.PENDING} +
setFeatured(ReviewStatus.APPROVED)}> + +
+
setFeatured(ReviewStatus.DENIED)}> + +
+ {/if} + {#if data.tool?.review === ReviewStatus.APPROVED || data.tool?.review === ReviewStatus.DENIED} +
setFeatured(ReviewStatus.PRIVATE)}> + +
+ {/if} + {/if} + {#if data.tool?.createdByMe && data.tool?.review === ReviewStatus.PRIVATE} +
{ + const confirmed = confirm( + "Are you sure you want to request this tool to be featured? Make sure you have tried the tool and that it works as expected. We will review your request once submitted." + ); + + if (!confirmed) { + return; + } + + setFeatured(ReviewStatus.PENDING); + }} + > + +
+ {/if} +
+
+
+ {#if !currentModelSupportTools} + + You are currently not using a model that supports tools. Activate one + here. + + {:else} +

+ Tools are applications that the model can choose to call while you are chatting with it. +

+ {/if} + {#if data.tool.description} +
+

Description

+

{data.tool.description}

+
+ {/if} + +
+

Direct URL

+ +

Share this link with people to use your tool.

+
+
+ +
+ +
+ Copy +
+
+
+
+
+
+
+
diff --git a/src/routes/tools/[toolId]/edit/+page.svelte b/src/routes/tools/[toolId]/edit/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3391d0d28ea8a03725e1448c382814058cbde19a --- /dev/null +++ b/src/routes/tools/[toolId]/edit/+page.svelte @@ -0,0 +1,20 @@ + + + window.history.back()} + width="h-[95dvh] w-[90dvw] overflow-hidden rounded-2xl bg-white shadow-2xl outline-none sm:h-[85dvh] xl:w-[1200px] 2xl:h-[75dvh]" + closeButton +> + { + window.history.back(); + }} + /> + diff --git a/src/routes/tools/new/+page.svelte b/src/routes/tools/new/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2aedd25649af88021b6bd844a82c920c31641395 --- /dev/null +++ b/src/routes/tools/new/+page.svelte @@ -0,0 +1,11 @@ + + + window.history.back()} + width="h-[95dvh] w-[90dvw] overflow-hidden rounded-2xl bg-white shadow-2xl outline-none sm:h-[85dvh] xl:w-[1200px] 2xl:h-[75dvh]" +> + window.history.back()} /> + diff --git a/src/styles/highlight-js.css b/src/styles/highlight-js.css new file mode 100644 index 0000000000000000000000000000000000000000..b262688368e9a946d72b21ae70fba7d711072fbb --- /dev/null +++ b/src/styles/highlight-js.css @@ -0,0 +1 @@ +@import "highlight.js/styles/atom-one-dark"; diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100644 index 0000000000000000000000000000000000000000..55a970baaa96e3c7a8158937c7f5c49c90a52010 --- /dev/null +++ b/src/styles/main.css @@ -0,0 +1,37 @@ +@import "./highlight-js.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + .btn { + @apply inline-flex flex-shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap outline-none transition-all focus:ring disabled:cursor-default; + } + + .active-model { + @apply border-blue-500 bg-blue-500/5 hover:bg-blue-500/10; + } + + .file-hoverable { + @apply hover:bg-gray-500/10; + } + + .base-tool { + @apply flex h-[1.6rem] items-center gap-[.2rem] whitespace-nowrap border border-transparent text-xs outline-none transition-all focus:outline-none active:outline-none dark:hover:text-gray-300 sm:hover:text-purple-600; + } + + .active-tool { + @apply rounded-full !border-purple-200 bg-purple-100 pl-1 pr-2 text-purple-600 hover:text-purple-600 dark:!border-purple-700 dark:bg-purple-600/40 dark:text-purple-200; + } +} + +@layer utilities { + .scrollbar-custom { + @apply scrollbar-thin scrollbar-track-transparent scrollbar-thumb-black/10 scrollbar-thumb-rounded-full scrollbar-w-1 hover:scrollbar-thumb-black/20 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20; + } +} + +.katex-display { + overflow: auto hidden; +} diff --git a/static/chatui/apple-touch-icon.png b/static/chatui/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8d739ac80519f3a3ef84410759f8f5daa553a2a2 --- /dev/null +++ b/static/chatui/apple-touch-icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38ce30784eebfed8f3ddb6759cb95685a8c2b00f3a93b2d13d8358e7c5e3d0e4 +size 1609 diff --git a/static/chatui/favicon.ico b/static/chatui/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6038067546406ed9a8b5c0449888129f1c8eb3e7 Binary files /dev/null and b/static/chatui/favicon.ico differ diff --git a/static/chatui/favicon.svg b/static/chatui/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..197865e59972001414c32b31fd2eef45d5c5d1c7 --- /dev/null +++ b/static/chatui/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/chatui/icon-128x128.png b/static/chatui/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..653cce8ca4f55a2133cf3308b46bd7e3efad419f --- /dev/null +++ b/static/chatui/icon-128x128.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc1cebb3ff19349040c8b41b81a52817a749cd7631e5409cbf7db0c680c64a18 +size 1179 diff --git a/static/chatui/icon-256x256.png b/static/chatui/icon-256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..e35962db6cc045ed6d40ff9588a1c5437c0a8a0b --- /dev/null +++ b/static/chatui/icon-256x256.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bc6dcb0af8a7acf4e9135e632589f86dfc1b4cff3105a352fc58dddf5575589 +size 2316 diff --git a/static/chatui/icon-512x512.png b/static/chatui/icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..d91e3cf057473e813c8c8cd4fcc6334cc88fa796 --- /dev/null +++ b/static/chatui/icon-512x512.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba23f927ad462e43bd6b0c3a20c6acafb8b333bb10cca7dfdd21bcdc364addd2 +size 4808 diff --git a/static/chatui/icon.svg b/static/chatui/icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..197865e59972001414c32b31fd2eef45d5c5d1c7 --- /dev/null +++ b/static/chatui/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/chatui/logo.svg b/static/chatui/logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..73c4c5f74129e827d85c0ce51c9091ae9c4d0bfa --- /dev/null +++ b/static/chatui/logo.svg @@ -0,0 +1,6 @@ + + + diff --git a/static/chatui/manifest.json b/static/chatui/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..7b2cd63572d3ac55b0f8adb70db1c2fb76b4e316 --- /dev/null +++ b/static/chatui/manifest.json @@ -0,0 +1,24 @@ +{ + "background_color": "#ffffff", + "name": "Chat UI", + "short_name": "Chat UI", + "display": "standalone", + "start_url": "/", + "icons": [ + { + "src": "/chatui/icon-128x128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "/chatui/icon-256x256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "/chatui/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/static/huggingchat/apple-touch-icon.png b/static/huggingchat/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..41eecbd4b5828c123bf09bbac0ad2378fbd55130 --- /dev/null +++ b/static/huggingchat/apple-touch-icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08a796f8cd63c025022f7c05c8b8494e73c2b1f5069b50cc1f3120d6a960448d +size 2697 diff --git a/static/huggingchat/assistants-thumbnail.png b/static/huggingchat/assistants-thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..5accc3d278f04a359dfdf1410ad7f50177f52892 --- /dev/null +++ b/static/huggingchat/assistants-thumbnail.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eac57333013a9eba9db97d68588d37cdb3b1d9658fed41e3e69eb98d194d34e4 +size 211156 diff --git a/static/huggingchat/favicon.ico b/static/huggingchat/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8b6ffd928f6e7734ce06e7560f690c0e9e20447a Binary files /dev/null and b/static/huggingchat/favicon.ico differ diff --git a/static/huggingchat/favicon.svg b/static/huggingchat/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..8311a549f98d44cd6f03e883e141b3b86abca7d0 --- /dev/null +++ b/static/huggingchat/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/huggingchat/icon-128x128.png b/static/huggingchat/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..249f359a82920efa53c394c6f4c3f8829f79dca0 --- /dev/null +++ b/static/huggingchat/icon-128x128.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b29470941a3fa93e445cba9431fb216a3310561f2a6e6ffb13f9c4f2635c08ef +size 1987 diff --git a/static/huggingchat/icon-144x144.png b/static/huggingchat/icon-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..802f30668a48c3b22aff2b320561d1c6a58761f9 --- /dev/null +++ b/static/huggingchat/icon-144x144.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dac31b23945b49e5b6b2c9c8072ae214eafea69b04278989bfe5b59346d688fa +size 4450 diff --git a/static/huggingchat/icon-192x192.png b/static/huggingchat/icon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..c5a293cb6b541806f435b0de5d8abb99d6ac548b --- /dev/null +++ b/static/huggingchat/icon-192x192.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:281dc76253360885f124ba19670f6410b0283eaf5e60503d2f2835f797eeff84 +size 5892 diff --git a/static/huggingchat/icon-256x256.png b/static/huggingchat/icon-256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..a385e6eae5eb43ceea5a17e4543cd98cc2f05d19 --- /dev/null +++ b/static/huggingchat/icon-256x256.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21c1b7d723da49ec890adda367604cdd5adbea251d526049423874c9603f862f +size 3766 diff --git a/static/huggingchat/icon-36x36.png b/static/huggingchat/icon-36x36.png new file mode 100644 index 0000000000000000000000000000000000000000..49b996b80a132810d2d571b38c9946548928494a --- /dev/null +++ b/static/huggingchat/icon-36x36.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da0514109a0ef5b919a8fcf94c1e3f7c4cf08e42e44e259bb89dd65441051f75 +size 1152 diff --git a/static/huggingchat/icon-48x48.png b/static/huggingchat/icon-48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..dd116ca73649c394e43e0694c690c812eb2fe670 --- /dev/null +++ b/static/huggingchat/icon-48x48.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1634899dee70e0591f8780071c938909ce30f5ce50fcff68d14cd22a7c5dcc8d +size 1411 diff --git a/static/huggingchat/icon-512x512.png b/static/huggingchat/icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..3625ce441ddcdede48f0dea0100d945615fc0abd --- /dev/null +++ b/static/huggingchat/icon-512x512.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd8cb0d6ea945d929e559377266951b34605b1ac0c80d56a036b3138195aa7a1 +size 7719 diff --git a/static/huggingchat/icon-72x72.png b/static/huggingchat/icon-72x72.png new file mode 100644 index 0000000000000000000000000000000000000000..e53185bf10e8d706efe4e839dadf407319baba9e --- /dev/null +++ b/static/huggingchat/icon-72x72.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:162723246818f482dd19d600b84f415284674d0d40c2de260f5841bed22852e8 +size 2284 diff --git a/static/huggingchat/icon-96x96.png b/static/huggingchat/icon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..e1fde49302537da3fe37be35f847f694682ed77d --- /dev/null +++ b/static/huggingchat/icon-96x96.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c670b9a655887043bec0e6c8c3c5ce032cdf0fb09d1b6a0b50540616355ebbdd +size 3024 diff --git a/static/huggingchat/icon.svg b/static/huggingchat/icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..abffd8f2bf0ab11e43f370753e32ee8d2b24e5a3 --- /dev/null +++ b/static/huggingchat/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/huggingchat/logo.svg b/static/huggingchat/logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..49765f64684f6d2465b0e4cf6ecf8017638f8429 --- /dev/null +++ b/static/huggingchat/logo.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/static/huggingchat/manifest.json b/static/huggingchat/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..09888cf128babbc5d2831118b4615df9ede082ad --- /dev/null +++ b/static/huggingchat/manifest.json @@ -0,0 +1,54 @@ +{ + "background_color": "#ffffff", + "name": "HuggingChat", + "short_name": "HuggingChat", + "display": "standalone", + "start_url": "/chat", + "icons": [ + { + "src": "/chat/huggingchat/icon-36x36.png", + "sizes": "36x36", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-48x48.png", + "sizes": "48x48", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-128x128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-256x256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/static/huggingchat/thumbnail.png b/static/huggingchat/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..7c84f02863c69dc8a94a732e75c3fe3ad66a2e1a --- /dev/null +++ b/static/huggingchat/thumbnail.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7df3570943d07ab30c4d5e54008809452e5032d29612135015fa662fb23805c7 +size 13352 diff --git a/static/huggingchat/tools-thumbnail.png b/static/huggingchat/tools-thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..d89c76c1183856d8bcc297afff95f08ce3b58953 --- /dev/null +++ b/static/huggingchat/tools-thumbnail.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73f0a81cff3b991dd82d7d3396f5e1c72b04b03fc4515f46e9646293ddc200ff +size 439594 diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000000000000000000000000000000000000..115bc5930d505a0cbd6bde8ce8ac290bec30724a --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,46 @@ +import adapter from "@sveltejs/adapter-node"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import dotenv from "dotenv"; +import { execSync } from "child_process"; + +dotenv.config({ path: "./.env.local" }); +dotenv.config({ path: "./.env" }); + +function getCurrentCommitSHA() { + try { + return execSync("git rev-parse HEAD").toString(); + } catch (error) { + console.error("Error getting current commit SHA:", error); + return "unknown"; + } +} + +process.env.PUBLIC_VERSION ??= process.env.npm_package_version; +process.env.PUBLIC_COMMIT_SHA ??= getCurrentCommitSHA(); + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + adapter: adapter(), + + paths: { + base: process.env.APP_BASE || "", + relative: false, + }, + csrf: { + // handled in hooks.server.ts, because we can have multiple valid origins + checkOrigin: false, + }, + csp: { + directives: { + ...(process.env.ALLOW_IFRAME === "true" ? {} : { "frame-ancestors": ["'none'"] }), + }, + }, + }, +}; + +export default config; diff --git a/tailwind.config.cjs b/tailwind.config.cjs new file mode 100644 index 0000000000000000000000000000000000000000..86884397e45eba930b6088af647be68a8e6b9efc --- /dev/null +++ b/tailwind.config.cjs @@ -0,0 +1,24 @@ +const defaultTheme = require("tailwindcss/defaultTheme"); +const colors = require("tailwindcss/colors"); + +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: "class", + mode: "jit", + content: ["./src/**/*.{html,js,svelte,ts}"], + theme: { + extend: { + colors: { + primary: colors[process.env.PUBLIC_APP_COLOR], + }, + fontSize: { + xxs: "0.625rem", + smd: "0.94rem", + }, + }, + }, + plugins: [ + require("tailwind-scrollbar")({ nocompatible: true }), + require("@tailwindcss/typography"), + ], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..2e4b2d5d934e5919d887560fa339aac799ce36e4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2018" + }, + "exclude": ["vite.config.ts"] + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..802f308f95dd099d67f9a9f0fc5e19d25a2c6a1d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,40 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import Icons from "unplugin-icons/vite"; +import { promises } from "fs"; +import { defineConfig } from "vitest/config"; + +// used to load fonts server side for thumbnail generation +function loadTTFAsArrayBuffer() { + return { + name: "load-ttf-as-array-buffer", + async transform(_src, id) { + if (id.endsWith(".ttf")) { + return `export default new Uint8Array([ + ${new Uint8Array(await promises.readFile(id))} + ]).buffer`; + } + }, + }; +} + +export default defineConfig({ + plugins: [ + sveltekit(), + Icons({ + compiler: "svelte", + }), + loadTTFAsArrayBuffer(), + ], + optimizeDeps: { + include: ["uuid", "@huggingface/transformers", "sharp", "@gradio/client"], + }, + server: { + open: "/", + }, + test: { + setupFiles: ["./scripts/setupTest.ts"], + deps: { inline: ["@sveltejs/kit"] }, + globals: true, + testTimeout: 10000, + }, +});