diff --git a/.env b/.env deleted file mode 100644 index 99eaf41c..00000000 --- a/.env +++ /dev/null @@ -1,38 +0,0 @@ -# Application -NODE_ENV=production -NEXT_PUBLIC_BASE_URL=https://klz-cables.com -UMAMI_WEBSITE_ID=e4a2cd1c-59fb-4e5b-bac5-9dfd1d02dd81 -UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me -SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1 -LOG_LEVEL=info -NEXT_PUBLIC_FEEDBACK_ENABLED=false -NEXT_PUBLIC_RECORD_MODE_ENABLED=false -NPM_TOKEN=263e7f75d8ada27f3a2e71fd6bd9d95298d48a4d - -# SMTP Configuration -MAIL_HOST=smtp.eu.mailgun.org -MAIL_PORT=587 -MAIL_USERNAME=postmaster@mg.mintel.me -MAIL_PASSWORD=4592fcb94599ee1a45b4ac2386fd0a64-102c75d8-ca2870e6 -MAIL_FROM="KLZ Cables " -MAIL_RECIPIENTS=marc@cablecreations.de,info@klz-cables.com - -# ──────────────────────────────────────────────────────────────────────────── -# Payload Infrastructure (Dockerized) -# ──────────────────────────────────────────────────────────────────────────── -# The POSTGRES_URI and PAYLOAD_SECRET are automatically constructed and injected -# by docker-compose.yml using these base DB credentials, so you don't need to -# manually write the connection strings here. -PAYLOAD_DB_NAME=payload -PAYLOAD_DB_USER=payload -PAYLOAD_DB_PASSWORD=120in09oenaoinsd9iaidon - -# ──────────────────────────────────────────────────────────────────────────── -# Hetzner S3 Object Storage -# ──────────────────────────────────────────────────────────────────────────── -S3_ENDPOINT=https://fsn1.your-objectstorage.com -S3_ACCESS_KEY=ROB3MSWMEIGRL7N94ZKS -S3_SECRET_KEY=9QJV3NE8xeLxhyufhNU7lsUB0RffJxPhGuEuFSH3 -S3_BUCKET=mintel -S3_REGION=fsn1 -S3_PREFIX=klz-cables \ No newline at end of file diff --git a/.env.example b/.env.example index 35710677..ab85e436 100644 --- a/.env.example +++ b/.env.example @@ -48,6 +48,12 @@ GATEKEEPER_PASSWORD=klz2026 SENTRY_DSN= # SENTRY_ENVIRONMENT is set automatically by CI +# ──────────────────────────────────────────────────────────────────────────── +# AI Agent (Payload CMS Agent via OpenRouter) +# ──────────────────────────────────────────────────────────────────────────── +# Required for the Payload CMS AI Chat Agent +MISTRAL_API_KEY= + # ──────────────────────────────────────────────────────────────────────────── # Payload Infrastructure (Dockerized) # ──────────────────────────────────────────────────────────────────────────── diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml deleted file mode 100644 index 16b6b8ab..00000000 --- a/.gitea/workflows/ci.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: CI - Lint, Typecheck & Test - -on: - pull_request: - -concurrency: - group: deploy-pipeline - cancel-in-progress: true - -jobs: - quality-assurance: - runs-on: docker - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v3 - with: - version: 10 - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: 🔐 Configure Private Registry - run: | - echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc - echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc - - - name: Install dependencies - run: pnpm install --no-frozen-lockfile - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: 🧪 QA Checks - env: - TURBO_TELEMETRY_DISABLED: "1" - run: npx turbo run check:mdx lint typecheck test --cache-dir=".turbo" - - - name: 🏗️ Build - run: pnpm build - - - name: ♿ Accessibility Check - run: pnpm start-server-and-test start http://localhost:3000 "pnpm check:a11y http://localhost:3000" - - - name: ♿ WCAG Sitemap Audit - run: pnpm start-server-and-test start http://localhost:3000 "pnpm run check:wcag http://localhost:3000" -# monitor trigger diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index ddfced86..39302c2c 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -14,8 +14,8 @@ on: default: 'false' env: - PUPPETEER_SKIP_DOWNLOAD: "true" - COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com" + PUPPETEER_SKIP_DOWNLOAD: 'true' + COREPACK_NPM_REGISTRY: 'https://registry.npmmirror.com' concurrency: group: deploy-pipeline @@ -29,14 +29,16 @@ jobs: name: 🔍 Prepare runs-on: docker outputs: - target: ${{ steps.determine.outputs.target }} - image_tag: ${{ steps.determine.outputs.image_tag }} - env_file: ${{ steps.determine.outputs.env_file }} - traefik_host: ${{ steps.determine.outputs.traefik_host }} - traefik_rule: ${{ steps.determine.outputs.traefik_rule }} - next_public_url: ${{ steps.determine.outputs.next_public_url }} - project_name: ${{ steps.determine.outputs.project_name }} - short_sha: ${{ steps.determine.outputs.short_sha }} + target: ${{ steps.determine.outputs.target }} + image_tag: ${{ steps.determine.outputs.image_tag }} + env_file: ${{ steps.determine.outputs.env_file }} + traefik_host: ${{ steps.determine.outputs.traefik_host }} + traefik_rule: ${{ steps.determine.outputs.traefik_rule }} + next_public_url: ${{ steps.determine.outputs.next_public_url }} + project_name: ${{ steps.determine.outputs.project_name }} + short_sha: ${{ steps.determine.outputs.short_sha }} + slug: ${{ steps.determine.outputs.slug }} + gatekeeper_host: ${{ steps.determine.outputs.gatekeeper_host }} container: image: catthehacker/ubuntu:act-latest steps: @@ -83,7 +85,7 @@ jobs: SLUG=$(echo "$REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//') IMAGE_TAG="branch-${SLUG}-${SHORT_SHA}" ENV_FILE=".env.branch-${SLUG}" - TRAEFIK_HOST="${SLUG}.branch.mintel.me" + TRAEFIK_HOST="${SLUG}.branch.klz-cables.com" fi # Standardize Traefik Rule (escaped backticks for Traefik v3) @@ -94,7 +96,7 @@ jobs: TRAEFIK_RULE='Host(`'"$TRAEFIK_HOST"'`)' PRIMARY_HOST="$TRAEFIK_HOST" fi - + GATEKEEPER_HOST="gatekeeper.$PRIMARY_HOST" { @@ -113,6 +115,7 @@ jobs: echo "project_name=$PRJ-$TARGET" fi echo "short_sha=$SHORT_SHA" + echo "slug=$SLUG" } >> "$GITHUB_OUTPUT" # ⏳ Wait for Upstream Packages/Images if Tagged @@ -156,6 +159,8 @@ jobs: needs: prepare if: needs.prepare.outputs.target != 'skip' runs-on: docker + env: + PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium container: image: catthehacker/ubuntu:act-latest steps: @@ -181,12 +186,15 @@ jobs: - name: 🔒 Security Audit run: pnpm audit --audit-level high || echo "⚠️ Audit found vulnerabilities (non-blocking)" + + - name: 🧹 Clean Workspace + run: rm -rf .next .turbo || true + - name: 🧪 QA Checks if: github.event.inputs.skip_checks != 'true' env: - TURBO_TELEMETRY_DISABLED: "1" + TURBO_TELEMETRY_DISABLED: '1' run: npx turbo run lint typecheck test --cache-dir=".turbo" - # ────────────────────────────────────────────────────────────────────────────── # JOB 3: Build & Push # ────────────────────────────────────────────────────────────────────────────── @@ -203,7 +211,8 @@ jobs: - name: 🐳 Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: 🔐 Registry Login - run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin + run: | + echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin - name: 🏗️ Build and Push uses: docker/build-push-action@v5 with: @@ -219,7 +228,22 @@ jobs: NPM_TOKEN=${{ secrets.NPM_TOKEN }} tags: registry.infra.mintel.me/mintel/klz-2026:${{ needs.prepare.outputs.image_tag }} secrets: | - "NPM_TOKEN=${{ secrets.NPM_TOKEN }}" + NPM_TOKEN=${{ secrets.NPM_TOKEN }} + - name: 🔄 Build and Push Migrator + uses: docker/build-push-action@v5 + with: + context: . + push: true + provenance: false + platforms: linux/amd64 + target: migrator + build-args: | + NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }} + NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} + NPM_TOKEN=${{ secrets.NPM_TOKEN }} + tags: registry.infra.mintel.me/mintel/klz-2026:migrate-${{ needs.prepare.outputs.image_tag }} + secrets: | + NPM_TOKEN=${{ secrets.NPM_TOKEN }} # ────────────────────────────────────────────────────────────────────────────── # JOB 4: Deploy @@ -231,44 +255,56 @@ jobs: container: image: catthehacker/ubuntu:act-latest env: - TARGET: ${{ needs.prepare.outputs.target }} - IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} - PROJECT_NAME: ${{ needs.prepare.outputs.project_name }} + TARGET: ${{ needs.prepare.outputs.target }} + IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} + PROJECT_NAME: ${{ needs.prepare.outputs.project_name }} NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }} - TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }} - GATEKEEPER_HOST: ${{ needs.prepare.outputs.gatekeeper_host }} - + TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }} + GATEKEEPER_HOST: ${{ needs.prepare.outputs.gatekeeper_host }} + SLUG: ${{ needs.prepare.outputs.slug }} + # Secrets mapping (Payload CMS) - PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }} - PAYLOAD_DB_NAME: ${{ secrets.PAYLOAD_DB_NAME || vars.PAYLOAD_DB_NAME || 'payload' }} - PAYLOAD_DB_USER: ${{ secrets.PAYLOAD_DB_USER || vars.PAYLOAD_DB_USER || 'payload' }} - PAYLOAD_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_DB_PASSWORD) || secrets.PAYLOAD_DB_PASSWORD || vars.PAYLOAD_DB_PASSWORD || 'payload' }} - + PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }} + PAYLOAD_DB_NAME: ${{ secrets.PAYLOAD_DB_NAME || vars.PAYLOAD_DB_NAME || 'payload' }} + PAYLOAD_DB_USER: ${{ secrets.PAYLOAD_DB_USER || vars.PAYLOAD_DB_USER || 'payload' }} + PAYLOAD_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_DB_PASSWORD) || secrets.PAYLOAD_DB_PASSWORD || vars.PAYLOAD_DB_PASSWORD || 'payload' }} + # Secrets mapping (Mail) - MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }} - MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }} - MAIL_USERNAME: ${{ secrets.SMTP_USER || vars.SMTP_USER }} - MAIL_PASSWORD: ${{ secrets.SMTP_PASS || vars.SMTP_PASS }} - MAIL_FROM: ${{ secrets.SMTP_FROM || vars.SMTP_FROM }} - MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT }} - + MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }} + MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }} + MAIL_USERNAME: ${{ secrets.SMTP_USER || vars.SMTP_USER }} + MAIL_PASSWORD: ${{ secrets.SMTP_PASS || vars.SMTP_PASS }} + MAIL_FROM: ${{ secrets.SMTP_FROM || vars.SMTP_FROM }} + MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT }} + # Monitoring - SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }} - + SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }} + # Gatekeeper GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} # Analytics - UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }} - UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }} + UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }} + UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }} + + # Search & AI + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY || vars.OPENROUTER_API_KEY }} + MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY || vars.MISTRAL_API_KEY }} + QDRANT_URL: ${{ secrets.QDRANT_URL || vars.QDRANT_URL || 'http://klz-qdrant:6333' }} + QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY || vars.QDRANT_API_KEY }} + REDIS_URL: ${{ secrets.REDIS_URL || vars.REDIS_URL || 'redis://klz-redis:6379' }} + KABELFACHMANN_MCP_URL: ${{ secrets.KABELFACHMANN_MCP_URL || vars.KABELFACHMANN_MCP_URL || 'http://klz-kabelfachmann:3007/sse' }} + # Container Registry (standalone) + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: 📝 Generate Environment shell: bash env: - TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }} - ENV_FILE: ${{ needs.prepare.outputs.env_file }} + TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }} + ENV_FILE: ${{ needs.prepare.outputs.env_file }} run: | # Middleware Selection Logic # Regular app routes get auth on non-production @@ -276,7 +312,7 @@ jobs: LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" ) COOKIE_DOMAIN=.$(echo $NEXT_PUBLIC_BASE_URL | sed 's|https://||') STD_MW="${PROJECT_NAME}-ratelimit,${PROJECT_NAME}-forward,${PROJECT_NAME}-compress" - + if [[ "$TARGET" == "production" ]]; then AUTH_MIDDLEWARE="$STD_MW" COMPOSE_PROFILES="" @@ -319,6 +355,14 @@ jobs: echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID" echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT" echo "" + echo "# Search & AI" + echo "OPENROUTER_API_KEY=$OPENROUTER_API_KEY" + echo "MISTRAL_API_KEY=$MISTRAL_API_KEY" + echo "QDRANT_URL=$QDRANT_URL" + echo "QDRANT_API_KEY=$QDRANT_API_KEY" + echo "REDIS_URL=$REDIS_URL" + echo "KABELFACHMANN_MCP_URL=$KABELFACHMANN_MCP_URL" + echo "" echo "TARGET=$TARGET" echo "SENTRY_ENVIRONMENT=$TARGET" echo "PROJECT_NAME=$PROJECT_NAME" @@ -338,16 +382,45 @@ jobs: cat .env.deploy echo "----------------------------" + - name: 🔐 Registry Auth + id: auth + run: | + echo "Testing available secrets against git.infra.mintel.me Docker registry..." + TOKENS="${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}" + USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel" + + VALID_TOKEN="" + VALID_USER="" + for T in $TOKENS; do + if [ -n "$T" ]; then + for U in $USERS; do + if [ -n "$U" ]; then + if echo "$T" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then + VALID_TOKEN="$T" + VALID_USER="$U" + break 2 + fi + fi + done + fi + done + if [ -z "$VALID_TOKEN" ]; then echo "❌ All tokens failed to authenticate!"; exit 1; fi + echo "token=$VALID_TOKEN" >> $GITHUB_OUTPUT + echo "user=$VALID_USER" >> $GITHUB_OUTPUT + - name: 🚀 SSH Deploy shell: bash env: - ENV_FILE: ${{ needs.prepare.outputs.env_file }} + ENV_FILE: ${{ needs.prepare.outputs.env_file }} run: | mkdir -p ~/.ssh echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null + # Determine deployment paths + echo "Preparing deployment for $TARGET..." + # Transfer and Restart if [[ "$TARGET" == "production" ]]; then SITE_DIR="/home/deploy/sites/klz-cables.com" @@ -356,17 +429,16 @@ jobs: elif [[ "$TARGET" == "staging" ]]; then SITE_DIR="/home/deploy/sites/staging.klz-cables.com" else - SITE_DIR="/home/deploy/sites/branch.klz-cables.com/${SLUG:-unknown}" + SITE_DIR="/home/deploy/sites/branch.klz-cables.com/$SLUG" fi + # Transfer files ssh root@alpha.mintel.me "mkdir -p $SITE_DIR" - scp .env.deploy root@alpha.mintel.me:$SITE_DIR/$ENV_FILE scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml - - ssh root@alpha.mintel.me "cd $SITE_DIR && echo '${{ secrets.REGISTRY_PASS }}' | docker login registry.infra.mintel.me -u '${{ secrets.REGISTRY_USER }}' --password-stdin" - ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull" - ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans" - + + # Execute remote commands — alpha is pre-logged into registry.infra.mintel.me + ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE pull && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE up -d --remove-orphans" + # Sanitize Payload Migrations: Replace 'dev' push entries with proper migration names. # Without this, Payload prompts interactively for confirmation and blocks forever in Docker. DB_CONTAINER="${{ needs.prepare.outputs.project_name }}-klz-db-1" @@ -379,40 +451,32 @@ jobs: echo " Attempt $i/15..." sleep 2 done - + echo "🔧 Sanitizing payload_migrations table (if exists)..." REMOTE_DB_USER=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_USER=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload") REMOTE_DB_NAME=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_NAME=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload") REMOTE_DB_USER="${REMOTE_DB_USER:-payload}" REMOTE_DB_NAME="${REMOTE_DB_NAME:-payload}" - # Auto-detect migrations from src/migrations/*.ts - BATCH=1 - VALUES="" - for f in $(ls src/migrations/*.ts 2>/dev/null | sort); do - NAME=$(basename "$f" .ts) - [ -n "$VALUES" ] && VALUES="$VALUES," - VALUES="$VALUES ('$NAME', $BATCH)" - ((BATCH++)) - done + # Run Payload migrations via a temporary container before restarting the app. + # This ensures fresh branch deployments (empty DBs) get their schema on first deploy. + echo "🔄 Running Payload migrations..." + MIGRATOR_IMAGE="registry.infra.mintel.me/mintel/klz-2026:migrate-$IMAGE_TAG" + + ssh root@alpha.mintel.me " + echo '${{ steps.auth.outputs.token }}' | docker login registry.infra.mintel.me -u '${{ steps.auth.outputs.user }}' --password-stdin 2>/dev/null || true + docker pull $MIGRATOR_IMAGE + docker run --rm \ + --network ${PROJECT_NAME}_default \ + --env-file $SITE_DIR/$ENV_FILE \ + $MIGRATOR_IMAGE \ + && echo '✅ Migrations complete.' \ + || echo '⚠️ Migrations failed or already up-to-date — continuing.' + " - if [ -n "$VALUES" ]; then - ssh root@alpha.mintel.me "docker exec $DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME -c \" - DO \\\$\\\$ BEGIN - DELETE FROM payload_migrations WHERE batch = -1; - INSERT INTO payload_migrations (name, batch) - SELECT name, batch FROM (VALUES $VALUES) AS v(name, batch) - WHERE NOT EXISTS (SELECT 1 FROM payload_migrations pm WHERE pm.name = v.name); - EXCEPTION WHEN undefined_table THEN - RAISE NOTICE 'payload_migrations table does not exist yet — skipping sanitization'; - END \\\$\\\$; - \"" || echo "⚠️ Migration sanitization skipped (table may not exist yet)" - fi - # Restart app to pick up clean migration state - APP_CONTAINER="${{ needs.prepare.outputs.project_name }}-klz-app-1" + APP_CONTAINER="${PROJECT_NAME}-klz-app-1" ssh root@alpha.mintel.me "docker restart $APP_CONTAINER" - ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'" - name: 🧹 Post-Deploy Cleanup (Runner) @@ -461,7 +525,7 @@ jobs: with: path: /usr/bin/chromium key: ${{ runner.os }}-chromium-native-${{ hashFiles('package.json') }} - + - name: 🔍 Install Chromium (Native & ARM64) if: steps.cache-chromium.outputs.cache-hit != 'true' run: | @@ -571,10 +635,8 @@ jobs: STATUS_LINE="All checks passed" fi - TITLE="$EMOJI klz-cables.com $VERSION → $TARGET" - MESSAGE="$STATUS_LINE - Deploy: $DEPLOY | Smoke: $SMOKE | Perf: $PERF - $URL" + TITLE="$EMOJI klz-cables.com $VERSION -> $TARGET" + MESSAGE="$STATUS_LINE | Deploy: $DEPLOY | Smoke: $SMOKE | $URL" curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ -F "title=$TITLE" \ diff --git a/.gitignore b/.gitignore index 32e4b271..21c62eb0 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,9 @@ html-errors*.json reference/ # Database backups backups/ + +.env + +# Payload CMS auto-generated +# Knowledge base source files +kabelhandbuch.txt \ No newline at end of file diff --git a/.npmrc b/.npmrc index 70ea8b5b..ea651854 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1 @@ @mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/ -//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${NPM_TOKEN} diff --git a/Dockerfile b/Dockerfile index 3c5f997d..8b445f1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,16 @@ # Stage 1: Builder -FROM registry.infra.mintel.me/mintel/nextjs:v1.8.20 AS base +FROM git.infra.mintel.me/mmintel/nextjs:latest AS base WORKDIR /app # Arguments for build-time configuration ARG NEXT_PUBLIC_BASE_URL ARG NEXT_PUBLIC_TARGET -ARG DIRECTUS_URL ARG UMAMI_WEBSITE_ID ARG UMAMI_API_ENDPOINT # Environment variables for Next.js build ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET -ENV DIRECTUS_URL=$DIRECTUS_URL ENV UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT ENV SKIP_RUNTIME_ENV_VALIDATION=true @@ -20,6 +18,7 @@ ENV CI=true # Copy lockfile and manifest for dependency installation caching COPY pnpm-lock.yaml package.json .npmrc* ./ +COPY patches* ./patches/ # Configure private registry and install dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ @@ -39,6 +38,11 @@ FROM base AS development ENV NODE_ENV=development CMD ["pnpm", "dev:local"] +# Stage: Migrator +FROM base AS migrator +ENV NODE_ENV=production +CMD ["pnpm", "cms:migrate"] + # Build application # Stage 3: Builder (Production) FROM base AS builder @@ -52,14 +56,9 @@ ENV UV_THREADPOOL_SIZE=3 RUN pnpm build # Stage 2: Runner -FROM registry.infra.mintel.me/mintel/runtime:v1.8.20 AS runner +FROM git.infra.mintel.me/mmintel/runtime:latest AS runner WORKDIR /app -# Create nextjs user and group (standardized in runtime image but ensuring local ownership) -USER root -RUN chown -R nextjs:nodejs /app -USER nextjs - ENV HOSTNAME="0.0.0.0" ENV PORT=3000 ENV NODE_ENV=production diff --git a/Dockerfile.dev b/Dockerfile.dev index 4b0dfa7a..bd6326c7 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,7 +1,7 @@ FROM node:20-alpine # Install essential build tools if needed (e.g., for node-gyp) -RUN apk add --no-cache libc6-compat python3 make g++ +RUN apk add --no-cache libc6-compat python3 make g++ curl WORKDIR /app diff --git a/app/(payload)/admin/importMap.js b/app/(payload)/admin/importMap.js index aecd3639..10c6ec49 100644 --- a/app/(payload)/admin/importMap.js +++ b/app/(payload)/admin/importMap.js @@ -1,57 +1,84 @@ -import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' -import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' -import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' -import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { default as default_9ed509b5e5f7d08a16335393f27586cc } from '../../../src/payload/components/Icon' -import { default as default_5470ea90f7a8fd882c2fe59ff2b1c5b9 } from '../../../src/payload/components/Logo' -import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' +import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'; +import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'; +import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'; +import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { default as default_9ed509b5e5f7d08a16335393f27586cc } from '../../../src/payload/components/Icon'; +import { default as default_5470ea90f7a8fd882c2fe59ff2b1c5b9 } from '../../../src/payload/components/Logo'; +import { ChatWindowProvider as ChatWindowProvider_d32a660df96f186e48bfc5b31626ccf5 } from '@mintel/payload-ai/components/ChatWindow'; +import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'; export const importMap = { - "@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e, - "@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e, - "@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e, - "@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "/src/payload/components/Icon#default": default_9ed509b5e5f7d08a16335393f27586cc, - "/src/payload/components/Logo#default": default_5470ea90f7a8fd882c2fe59ff2b1c5b9, - "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 -} + '@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell': + RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e, + '@payloadcms/richtext-lexical/rsc#RscEntryLexicalField': + RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e, + '@payloadcms/richtext-lexical/rsc#LexicalDiffComponent': + LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e, + '@payloadcms/richtext-lexical/client#BlocksFeatureClient': + BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient': + InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient': + HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#UploadFeatureClient': + UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#BlockquoteFeatureClient': + BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#RelationshipFeatureClient': + RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#LinkFeatureClient': + LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#ChecklistFeatureClient': + ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#OrderedListFeatureClient': + OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#UnorderedListFeatureClient': + UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#IndentFeatureClient': + IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#AlignFeatureClient': + AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#HeadingFeatureClient': + HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#ParagraphFeatureClient': + ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#InlineCodeFeatureClient': + InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#SuperscriptFeatureClient': + SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#SubscriptFeatureClient': + SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#StrikethroughFeatureClient': + StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#UnderlineFeatureClient': + UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#BoldFeatureClient': + BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#ItalicFeatureClient': + ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '/src/payload/components/Icon#default': default_9ed509b5e5f7d08a16335393f27586cc, + '/src/payload/components/Logo#default': default_5470ea90f7a8fd882c2fe59ff2b1c5b9, + '@mintel/payload-ai/components/ChatWindow#ChatWindowProvider': + ChatWindowProvider_d32a660df96f186e48bfc5b31626ccf5, + '@payloadcms/next/rsc#CollectionCards': CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1, +}; diff --git a/app/[locale]/blog/[slug]/page.tsx b/app/[locale]/blog/[slug]/page.tsx index e2601738..a2c51229 100644 --- a/app/[locale]/blog/[slug]/page.tsx +++ b/app/[locale]/blog/[slug]/page.tsx @@ -134,13 +134,13 @@ export default async function BlogPost({ params }: BlogPostProps) { {getReadingTime(rawTextContent)} min read {(new Date(post.frontmatter.date) > new Date() || post.frontmatter.public === false) && ( - <> - - - Draft Preview - - - )} + <> + + + Draft Preview + + + )} @@ -171,13 +171,13 @@ export default async function BlogPost({ params }: BlogPostProps) { {getReadingTime(rawTextContent)} min read {(new Date(post.frontmatter.date) > new Date() || post.frontmatter.public === false) && ( - <> - - - Draft Preview - - - )} + <> + + + Draft Preview + + + )} diff --git a/app/api/ai-search/route.ts b/app/api/ai-search/route.ts new file mode 100644 index 00000000..22bcdff3 --- /dev/null +++ b/app/api/ai-search/route.ts @@ -0,0 +1,218 @@ +import { NextResponse, NextRequest } from 'next/server'; // Added NextRequest +import { searchProducts } from '../../../src/lib/qdrant'; +import redis from '../../../src/lib/redis'; +import { z } from 'zod'; +import * as Sentry from '@sentry/nextjs'; +import { generateText } from 'ai'; +import { createOpenAI } from '@ai-sdk/openai'; +// @ts-expect-error - Local version of @mintel/payload-ai/tools/mcpAdapter might not have types published yet +import { createMcpTools } from '@mintel/payload-ai/tools/mcpAdapter'; + +export const dynamic = 'force-dynamic'; +export const maxDuration = 60; // Max allowed duration (Vercel) + +// Config and constants +const RATE_LIMIT_POINTS = 20; // 20 requests per minute +const RATE_LIMIT_DURATION = 60; // 1 minute window +const DAILY_BUDGET_LIMIT = 200; // max 200 requests per IP per day +const DAILY_BUDGET_DURATION = 60 * 60 * 24; // 24h +const MAX_CONVERSATION_MESSAGES = 20; // max messages in context +const MAX_RESPONSE_TOKENS = 300; // cap AI response length — keeps it chat-like + +// Removed requestSchema as it's replaced by direct parsing + +export async function POST(req: NextRequest) { + // Changed req type to NextRequest + try { + let body: any; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + const { messages, honeypot } = body; + + // Get client IP for rate limiting + const forwarded = req.headers.get('x-forwarded-for'); + const clientIp = forwarded?.split(',')[0]?.trim() || req.headers.get('x-real-ip') || 'unknown'; + + // 1. Basic Validation + if (!messages || !Array.isArray(messages) || messages.length === 0) { + return NextResponse.json({ error: 'Valid messages array is required' }, { status: 400 }); + } + + const latestMessage = messages[messages.length - 1].content; + const isBot = honeypot && honeypot.length > 0; + + // Check if the input itself is obviously spam/too long + if (latestMessage.length > 500) { + return NextResponse.json({ error: 'Message too long' }, { status: 400 }); + } + + // 2. Honeypot check + if (isBot) { + console.warn('Honeypot triggered in AI search'); + // Tarpit the bot + await new Promise((resolve) => setTimeout(resolve, 3000)); + return NextResponse.json({ + answerText: 'Vielen Dank für Ihre Anfrage.', + products: [], + }); + } + + // 3. Rate Limiting via Redis (IP-based) + try { + // Per-minute burst limit + const minuteKey = `ai_rate:${clientIp}:min`; + const minuteCount = await redis.incr(minuteKey); + if (minuteCount === 1) await redis.expire(minuteKey, RATE_LIMIT_DURATION); + + if (minuteCount > RATE_LIMIT_POINTS) { + return NextResponse.json( + { error: 'Zu viele Anfragen. Bitte warte einen Moment.' }, + { status: 429 }, + ); + } + + // Daily budget limit + const dayKey = `ai_rate:${clientIp}:day`; + const dayCount = await redis.incr(dayKey); + if (dayCount === 1) await redis.expire(dayKey, DAILY_BUDGET_DURATION); + + if (dayCount > DAILY_BUDGET_LIMIT) { + return NextResponse.json( + { error: 'Tägliches Limit erreicht. Bitte versuche es morgen erneut.' }, + { status: 429 }, + ); + } + } catch (redisError) { + console.error('Redis Rate Limiting Error:', redisError); + Sentry.captureException(redisError, { tags: { context: 'ai-search-rate-limit' } }); + // Fail open if Redis is down + } + + // 4. Cap conversation length to limit token usage + const cappedMessages = messages.slice(-MAX_CONVERSATION_MESSAGES); + + // 4. Fetch Context from Qdrant based on the latest message + let contextStr = ''; + let foundProducts: any[] = []; + + // Team context — hardcoded from translation data (no Payload collection for team) + const teamContextStr = ` +Das ECHTE KLZ Team: +- Michael Bodemer (Geschäftsführer) — Der Macher, packt an wenn es kompliziert wird, kennt Kabelnetze in- und auswendig +- Klaus Mintel (Geschäftsführer) — Der Fels in der Brandung, jahrzehntelange Erfahrung, stabiles Netzwerk`; + + try { + const searchResults = await searchProducts(latestMessage, 5); + + if (searchResults && searchResults.length > 0) { + const productDescriptions = searchResults + .filter((p) => p.payload?.type === 'product' || !p.payload?.type) + .map((p: any) => p.payload?.content) + .join('\n\n'); + + if (productDescriptions) { + contextStr = `KATALOG & PRODUKTE:\n${productDescriptions}`; + } + + foundProducts = searchResults + .filter((p) => (p.payload?.type === 'product' || !p.payload?.type) && p.payload?.data) + .map((p: any) => ({ + id: p.id as string, + title: p.payload?.data?.title as string, + sku: p.payload?.data?.sku as string, + slug: p.payload?.data?.slug as string, + })); + } + } catch (searchError) { + console.error('Qdrant Search Error:', searchError); + Sentry.captureException(searchError, { tags: { context: 'ai-search-qdrant' } }); + // We can still proceed without context if Qdrant fails + } + + // 5. Generate AI Response via OpenRouter (Mistral for DSGVO) + const systemPrompt = `Du bist "Ohm" — der digitale KI-Berater von KLZ Cables. Dein Name ist eine Anspielung auf die Einheit des elektrischen Widerstands. + +STIL & PERSÖNLICHKEIT: +- Antworte KURZ, KNAPP und PROFESSIONELL (maximal 2-3 Sätze). +- Schreibe wie in einem lockeren, aber kompetenten B2B-Chat (Du-Form ist okay, aber fachlich top). +- Kein Markdown, nur Fließtext. +- NIEMALS Platzhalter wie [Ihr Name], [Name], [Firma] verwenden. + +DEINE HAUPTAUFGABE: BERATEN, NICHT AUSFRAGEN! +- Wenn der Kunde ein Projekt nennt (z.B. "Windpark 30kV"), dann lies im KONTEXT nach, welche Kabel passen, und EMPFIEHL SIE DIREKT! (z.B. "Für 30kV Windparks nehmen wir meistens NA2XS(F)2Y."). +- Stelle NIEMALS mehr als EINE Rückfrage pro Nachricht. +- FRAGE NICHT nach abstrakten Dingen wie "Welchen Kabeltyp brauchst du?" -> DAS IST DEIN JOB, IHM DAS ZU SAGEN! +- FRAGE NICHT nach Längen oder genauen Trassen, es sei denn, der Kunde hat schon ganz klar gesagt, was er kaufen will. +- Biete aktiv Hilfe an: "Ich kann dir die passenden Querschnitte raussuchen, wenn du willst." +- Wenn technisches Wissen aus dem Kabelhandbuch benötigt wird, NUTZE UNBEDINGT eines der "kabelfachmann_*" Tools, anstatt zu raten oder zu behaupten du wüsstest es nicht! Das Tool weiss alles. + +VORGEHEN: +1. Prüfe den KONTEXT auf passende Katalog-Kabel für das Kundenprojekt. +2. Wenn du tiefgehendes Wissen zu einem Kabeltyp brauchst (z.B. Biegeradius, Normen, Querschnitte), rufe das Kabelfachmann-Tool auf. +3. Nenne direkt 1-2 passende Produktserien aus dem Kontext oder der Tool-Abfrage, die für diesen Fall Sinn machen. +4. Biete eine konkrete Hilfestellung an (z.B. Leitungsberechnung, Verfügbarkeitsprüfung) ODER stelle EINE einzige fachliche Rückfrage, um das Kabel weiter einzugrenzen (z.B. Alu oder Kupfer?). +5. Wenn das Projekt klar ist und die Kabeltypen besprochen sind, frag nach, ob ein Kollege (z.B. Micha) ein konkretes Angebot machen soll. + +GRENZEN: +- PRIVAT-ANFRAGEN: B2B only. Private Hausinstallationen lehnen wir freundlich ab. +- Keine Preise oder genauen Lieferzeiten versprechen. Immer auf die menschlichen Kollegen verweisen für finale Angebote. + +KONTEXT KABEL & TEAM: +${contextStr || 'Kein Katalogkontext verfügbar.'} +${teamContextStr} +`; + + const openrouterApiKey = process.env.OPENROUTER_API_KEY; + if (!openrouterApiKey) { + throw new Error('OPENROUTER_API_KEY is not set'); + } + + const openrouter = createOpenAI({ + baseURL: 'https://openrouter.ai/api/v1', + apiKey: openrouterApiKey, + }); + + let mcpTools: Record = {}; + const mcpUrl = process.env.KABELFACHMANN_MCP_URL || 'http://host.docker.internal:3007/sse'; + try { + const { tools } = await createMcpTools({ + name: 'kabelfachmann', + url: mcpUrl, + }); + mcpTools = tools; + } catch (e) { + console.warn('Failed to load MCP tools', e); + Sentry.captureException(e, { tags: { context: 'ai-search-mcp' } }); + } + + const { text } = await generateText({ + model: openrouter('openai/gpt-4o-mini'), + system: systemPrompt, + messages: cappedMessages.map((m: any) => ({ + role: m.role, + content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content), + })), + tools: mcpTools, + // @ts-expect-error - maxSteps might be missing in some versions of generateText types + maxSteps: 5, + temperature: 0.3, + maxTokens: MAX_RESPONSE_TOKENS, + }); + + // Return the AI's answer along with any found products + return NextResponse.json({ + answerText: text, + products: foundProducts, + }); + } catch (error) { + console.error('AI Search API Error:', error); + Sentry.captureException(error, { tags: { context: 'ai-search-api' } }); + return NextResponse.json( + { error: 'Ein interner Fehler ist aufgetreten. Bitte versuche es erneut.' }, + { status: 500 }, + ); + } +} diff --git a/app/api/health/cms/route.ts b/app/api/health/cms/route.ts index c247e36a..9b233273 100644 --- a/app/api/health/cms/route.ts +++ b/app/api/health/cms/route.ts @@ -27,7 +27,7 @@ export async function GET() { } } - const hasErrors = Object.values(checks).some(v => v.startsWith('error')); + const hasErrors = Object.values(checks).some((v) => v.startsWith('error')); return NextResponse.json( { status: hasErrors ? 'degraded' : 'ok', checks }, { status: hasErrors ? 503 : 200 }, diff --git a/app/api/sync-qdrant/route.ts b/app/api/sync-qdrant/route.ts new file mode 100644 index 00000000..b82900f4 --- /dev/null +++ b/app/api/sync-qdrant/route.ts @@ -0,0 +1,165 @@ +import { NextResponse } from 'next/server'; +import { getPayload } from 'payload'; +import configPromise from '../../../payload.config'; +import { upsertProductVector } from '../../../src/lib/qdrant'; + +export const dynamic = 'force-dynamic'; +export const maxDuration = 120; + +/** + * Internal endpoint called by the warmup script on every dev boot. + * Syncs posts, pages, and products from Payload CMS into Qdrant. + * NOT for form entries, media, or users. + */ +export async function GET() { + const results = { products: 0, posts: 0, pages: 0, errors: [] as string[] }; + + try { + const payload = await getPayload({ config: configPromise }); + + // ── Products ── + const { docs: products } = await payload.find({ + collection: 'products', + limit: 1000, + depth: 0, + where: { _status: { equals: 'published' } }, + }); + + for (const product of products) { + try { + const contentText = `${product.title} - SKU: ${product.sku}\n${product.description || ''}`; + await upsertProductVector(String(product.id), contentText, { + type: 'product', + data: { + title: product.title, + sku: product.sku, + slug: product.slug, + description: product.description, + }, + }); + results.products++; + } catch (e: any) { + results.errors.push(`product:${product.sku}: ${e.message}`); + } + } + + // ── Posts ── + const { docs: posts } = await payload.find({ + collection: 'posts', + limit: 1000, + depth: 0, + where: { _status: { equals: 'published' } }, + }); + + for (const post of posts) { + try { + const contentText = [ + `Blog-Artikel: ${post.title}`, + post.excerpt ? `Zusammenfassung: ${post.excerpt}` : '', + post.category ? `Kategorie: ${post.category}` : '', + ] + .filter(Boolean) + .join('\n'); + + await upsertProductVector(`post_${post.id}`, contentText, { + type: 'knowledge', + content: contentText, + data: { + title: post.title, + slug: post.slug, + }, + }); + results.posts++; + } catch (e: any) { + results.errors.push(`post:${post.slug}: ${e.message}`); + } + } + + // ── Pages ── + const { docs: pages } = await payload.find({ + collection: 'pages', + limit: 1000, + depth: 0, + where: { _status: { equals: 'published' } }, + }); + + for (const page of pages) { + try { + const contentText = [ + `Seite: ${page.title}`, + page.excerpt ? `Beschreibung: ${page.excerpt}` : '', + ] + .filter(Boolean) + .join('\n'); + + await upsertProductVector(`page_${page.id}`, contentText, { + type: 'knowledge', + content: contentText, + data: { + title: page.title, + slug: page.slug, + }, + }); + results.pages++; + } catch (e: any) { + results.errors.push(`page:${page.slug}: ${e.message}`); + } + } + + // ── Kabelhandbuch (Static Text) ── + const os = require('os'); + const path = require('path'); + const fs = require('fs'); + const crypto = await import('crypto'); + + const txtPath = path.join(process.cwd(), 'kabelhandbuch.txt'); + let manualChunks = 0; + + if (fs.existsSync(txtPath)) { + try { + const text = fs.readFileSync(txtPath, 'utf8'); + const chunks = text + .split(/\n\s*\n/) + .map((c: string) => c.trim()) + .filter((c: string) => c.length > 50); + + for (let i = 0; i < chunks.length; i++) { + const chunkText = chunks[i]; + const syntheticId = crypto.randomUUID(); + + await upsertProductVector(syntheticId, chunkText, { + type: 'knowledge', + content: chunkText, + data: { + title: `Kabelhandbuch Wissen - Bereich ${i + 1}`, + source: 'Kabelhandbuch KLZ.pdf', + }, + }); + manualChunks++; + } + console.log(`[Qdrant Sync] ✅ ${manualChunks} Kabelhandbuch-Chunks synced`); + } catch (e: any) { + results.errors.push(`kabelhandbuch: ${e.message}`); + } + } else { + console.log(`[Qdrant Sync] ⚠️ skipped Kabelhandbuch: ${txtPath} not found`); + } + + console.log( + `[Qdrant Sync] ✅ ${results.products} products, ${results.posts} posts, ${results.pages} pages synced, ${manualChunks} manual chunks synced`, + ); + + return NextResponse.json({ + success: true, + synced: { + products: results.products, + posts: results.posts, + pages: results.pages, + }, + errors: results.errors.length > 0 ? results.errors : undefined, + }); + } catch (error: any) { + console.error('[Qdrant Sync] ❌ Fatal error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/components/CMSConnectivityNotice.tsx b/components/CMSConnectivityNotice.tsx index a24545b5..5ebc9896 100644 --- a/components/CMSConnectivityNotice.tsx +++ b/components/CMSConnectivityNotice.tsx @@ -15,10 +15,12 @@ export default function CMSConnectivityNotice() { const isDebug = new URLSearchParams(window.location.search).has('cms_debug'); const isLocal = config.isDevelopment; const isTesting = config.isTesting; + const target = process.env.NEXT_PUBLIC_TARGET || ''; + const isBranch = target === 'branch'; - // Only proceed with check if it's developer context (Local or Testing) + // Only proceed with check if it's developer context (Local, Testing, or Branch preview) // Staging and Production should NEVER see this unless forced with ?cms_debug - if (!isLocal && !isTesting && !isDebug) return; + if (!isLocal && !isTesting && !isBranch && !isDebug) return; try { const response = await fetch('/api/health/cms'); @@ -58,8 +60,8 @@ export default function CMSConnectivityNotice() {

CMS Issue Detected

{errorMsg === 'relation "products" does not exist' - ? 'The database schema is missing. Please sync your local data to this environment.' - : errorMsg || 'The application cannot connect to the Directus CMS.'} + ? 'The database schema is missing. Please run migrations for this environment.' + : 'A content service is unavailable. Check the deployment logs for details.'}

+ + {/* Brand & Quality Sub-Footer */} + ); diff --git a/components/Header.tsx b/components/Header.tsx index e3d3688b..d99b29bb 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -9,6 +9,8 @@ import { useEffect, useState, useRef } from 'react'; import { cn } from './ui'; import { useAnalytics } from './analytics/useAnalytics'; import { AnalyticsEvents } from './analytics/analytics-events'; +import { Search } from 'lucide-react'; +import { AISearchResults } from './search/AISearchResults'; export default function Header() { const t = useTranslations('Navigation'); @@ -16,6 +18,7 @@ export default function Header() { const { trackEvent } = useAnalytics(); const [isScrolled, setIsScrolled] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [isSearchOpen, setIsSearchOpen] = useState(false); const mobileMenuRef = useRef(null); // Extract locale from pathname @@ -273,6 +276,19 @@ export default function Header() {
+ +
+ +
+ + setIsSearchOpen(false)} /> ); } diff --git a/components/home/Hero.tsx b/components/home/Hero.tsx index 778dc134..7f117db2 100644 --- a/components/home/Hero.tsx +++ b/components/home/Hero.tsx @@ -1,98 +1,221 @@ 'use client'; +import Scribble from '@/components/Scribble'; import { Button, Container, Heading, Section } from '@/components/ui'; import { useTranslations, useLocale } from 'next-intl'; import dynamic from 'next/dynamic'; import { useAnalytics } from '../analytics/useAnalytics'; import { AnalyticsEvents } from '../analytics/analytics-events'; +import { useState, useEffect, useRef, useCallback } from 'react'; +import { ChevronRight } from 'lucide-react'; +import { AISearchResults } from '../search/AISearchResults'; const HeroIllustration = dynamic(() => import('./HeroIllustration'), { ssr: false }); +const AIOrb = dynamic(() => import('../search/AIOrb'), { ssr: false }); export default function Hero({ data }: { data?: any }) { const t = useTranslations('Home.hero'); const locale = useLocale(); const { trackEvent } = useAnalytics(); + const [searchQuery, setSearchQuery] = useState(''); + const [isSearchOpen, setIsSearchOpen] = useState(false); + const [heroPlaceholder, setHeroPlaceholder] = useState( + 'Projekt beschreiben oder Kabel suchen...', + ); + const typingRef = useRef | null>(null); + + const HERO_PLACEHOLDERS = [ + 'Querschnittsberechnung für 110kV Trasse', // Hochspannung + 'Wie schwer ist NAYY 4x150?', + 'Ich plane einen Solarpark, was brauche ich?', // Projekt Solar + 'Unterschied zwischen N2XSY und NAY2XSY?', // Fach + 'Mittelspannungskabel für Windkraftanlage', // Windpark + 'Welches Aluminiumkabel für 20kV?', // Mittelspannung + ]; + + // Typing animation for the hero search placeholder + useEffect(() => { + if (searchQuery) { + setHeroPlaceholder('Projekt beschreiben oder Kabel suchen...'); + return; + } + + let textIdx = 0; + let charIdx = 0; + let deleting = false; + + const tick = () => { + const fullText = HERO_PLACEHOLDERS[textIdx]; + + if (deleting) { + charIdx--; + setHeroPlaceholder(fullText.substring(0, charIdx)); + } else { + charIdx++; + setHeroPlaceholder(fullText.substring(0, charIdx)); + } + + let delay = deleting ? 30 : 70; + + if (!deleting && charIdx === fullText.length) { + delay = 2500; + deleting = true; + } else if (deleting && charIdx === 0) { + deleting = false; + textIdx = (textIdx + 1) % HERO_PLACEHOLDERS.length; + delay = 400; + } + + typingRef.current = setTimeout(tick, delay); + }; + + typingRef.current = setTimeout(tick, 1500); + + return () => { + if (typingRef.current) clearTimeout(typingRef.current); + }; + }, [searchQuery]); + + const handleSearchSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (searchQuery.trim()) { + setIsSearchOpen(true); + } + }; + return ( -
- -
-
- - {data?.title ? ( - /g, '') - .replace(/<\/green>/g, ''), - }} - /> - ) : ( - t.rich('title', { - green: (chunks) => {chunks}, - }) - )} - -
-
-

- {data?.subtitle || t('subtitle')} -

-
-
+ <> +
+ +
+ + {data?.title ? ( + /g, + '', + ) + .replace( + /<\/green>/g, + '', + ), + }} + /> + ) : ( + t.rich('title', { + green: (chunks) => ( + + + {chunks} + +
+ +
+
+ ), + }) + )} +
+
+
+

+ {data?.subtitle || t('subtitle')} +

+
+
+
+ +
+ setSearchQuery(e.target.value)} + placeholder={heroPlaceholder} + className="flex-1 bg-transparent border-none text-white pl-20 pr-2 py-4 placeholder:text-white/50 focus:outline-none text-lg lg:text-xl" + autoFocus + /> -
-
- + + +
+
+ +
+
+ +
-
- + -
- -
- -
-
-
+
+
-
-
+ +
+
+
+
+
+ + setIsSearchOpen(false)} + initialQuery={searchQuery} + triggerSearch={true} + /> + ); } diff --git a/components/home/RecentPosts.tsx b/components/home/RecentPosts.tsx index 02111a09..74f189ae 100644 --- a/components/home/RecentPosts.tsx +++ b/components/home/RecentPosts.tsx @@ -74,11 +74,14 @@ export default async function RecentPosts({ locale, data }: RecentPostsProps) { suppressHydrationWarning className="px-3 py-1 text-white/80 text-[10px] md:text-xs font-bold uppercase tracking-widest border border-white/20 rounded-full bg-white/10 backdrop-blur-md" > - {new Date(post.frontmatter.date).toLocaleDateString(locale || 'de', { - year: 'numeric', - month: 'short', - day: 'numeric', - })} + {new Date(post.frontmatter.date).toLocaleDateString( + locale?.length === 2 ? locale : 'de', + { + year: 'numeric', + month: 'short', + day: 'numeric', + }, + )} {(new Date(post.frontmatter.date) > new Date() || post.frontmatter.public === false) && ( diff --git a/components/search/AIOrb.tsx b/components/search/AIOrb.tsx new file mode 100644 index 00000000..417cecea --- /dev/null +++ b/components/search/AIOrb.tsx @@ -0,0 +1,371 @@ +'use client'; + +import React, { useRef, useEffect, useCallback } from 'react'; + +interface AIOrbProps { + isThinking: boolean; + hasError?: boolean; +} + +function lerp(a: number, b: number, t: number) { + return a + (b - a) * t; +} + +// Simple noise function for organic movement +function noise(x: number, y: number, t: number): number { + return ( + Math.sin(x * 1.3 + t * 0.7) * Math.cos(y * 0.9 + t * 0.5) * 0.5 + + Math.sin(x * 2.7 + y * 1.1 + t * 1.3) * 0.25 + + Math.cos(x * 0.8 - y * 2.3 + t * 0.9) * 0.25 + ); +} + +// ── Particle ─────────────────────────────────────────────────── +interface Particle { + // Sphere position (target shape) + theta: number; + phi: number; + // Current position + x: number; + y: number; + z: number; + // Velocity + vx: number; + vy: number; + vz: number; + // Properties + size: number; + baseSize: number; + hue: number; // 0=blue, 1=green + brightness: number; + phase: number; + orbitSpeed: number; + noiseScale: number; +} + +function createParticles(count: number): Particle[] { + const particles: Particle[] = []; + + for (let i = 0; i < count; i++) { + // Fibonacci sphere distribution for even spacing + const golden = Math.PI * (3 - Math.sqrt(5)); + const y = 1 - (i / (count - 1)) * 2; + const radiusAtY = Math.sqrt(1 - y * y); + const theta = golden * i; + const phi = Math.acos(y); + + particles.push({ + theta, + phi, + x: Math.cos(theta) * radiusAtY, + y, + z: Math.sin(theta) * radiusAtY, + vx: 0, + vy: 0, + vz: 0, + size: 0.4 + Math.random() * 0.8, + baseSize: 0.4 + Math.random() * 0.8, + hue: Math.random() > 0.45 ? 0 : 1, + brightness: 0.5 + Math.random() * 0.5, + phase: Math.random() * Math.PI * 2, + orbitSpeed: (0.1 + Math.random() * 0.4) * (Math.random() > 0.5 ? 1 : -1), + noiseScale: 0.5 + Math.random() * 1.5, + }); + } + return particles; +} + +export default function AIOrb({ isThinking = false, hasError = false }: AIOrbProps) { + const canvasRef = useRef(null); + const wrapRef = useRef(null); + const animRef = useRef(0); + const particlesRef = useRef([]); + + const mouse = useRef({ x: 0.5, y: 0.5, hover: false }); + const state = useRef({ + pulse: 0, + hover: 0, + error: 0, + mouseX: 0.5, + mouseY: 0.5, + rotY: 0, + rotX: 0, + breathe: 0, + scatter: 0, + shake: 0, + }); + + const onMove = useCallback((e: React.PointerEvent) => { + const r = wrapRef.current?.getBoundingClientRect(); + if (!r) return; + mouse.current.x = (e.clientX - r.left) / r.width; + mouse.current.y = (e.clientY - r.top) / r.height; + }, []); + const onEnter = useCallback(() => { + mouse.current.hover = true; + }, []); + const onLeave = useCallback(() => { + mouse.current.hover = false; + mouse.current.x = 0.5; + mouse.current.y = 0.5; + }, []); + + const draw = useCallback( + function drawStep() { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const dpr = window.devicePixelRatio || 1; + const rect = canvas.getBoundingClientRect(); + const w = rect.width * dpr; + const h = rect.height * dpr; + if (canvas.width !== w || canvas.height !== h) { + canvas.width = w; + canvas.height = h; + } + + const cx = w / 2; + const cy = h / 2; + const minDim = Math.min(w, h); + // Reduced further to give maximum breathing room for glow + movement + const sphereR = minDim * 0.16; + const time = performance.now() / 1000; + const s = state.current; + const m = mouse.current; + + // ── Interpolate state ── + s.pulse = lerp(s.pulse, isThinking ? 1 : 0, 0.03); + s.hover = lerp(s.hover, m.hover ? 1 : 0, 0.12); + s.error = lerp(s.error, hasError ? 1 : 0, 0.05); + s.mouseX = lerp(s.mouseX, m.x, 0.12); + s.mouseY = lerp(s.mouseY, m.y, 0.12); + s.scatter = lerp(s.scatter, m.hover ? 0.8 : hasError ? 0.5 : 0, 0.06); + s.shake += 0.15 * s.error; + + // Global rotation — ALWAYS rotating + ALWAYS facing cursor + s.rotY += lerp(0.008, 0.04, Math.max(s.pulse, s.hover)); + const mouseRotY = (s.mouseX - 0.5) * 1.2; // always face cursor + const mouseRotX = (s.mouseY - 0.5) * 0.8; + + s.breathe += lerp(1.2, 3.0, s.pulse) / 60; + const breathe = Math.sin(s.breathe) * 0.5 + 0.5; + + // ── Clear ── + ctx.clearRect(0, 0, w, h); + + // ── Subtle core glow ── + const shakeX = Math.sin(s.shake * 17) * s.error * minDim * 0.02; + const glowCX = cx + shakeX; + const glowCY = cy; + // Clamp glow radius so it never exceeds ~48% of canvas (leaves padding for movement) + const glowR = Math.min( + sphereR * lerp(2.2, 4.0, Math.max(s.pulse, s.hover * 0.8)), + minDim * 0.48, + ); + const glowA = lerp(0.1, 0.4, Math.max(s.pulse, s.hover * 0.7, s.error * 0.8)); + const glow = ctx.createRadialGradient(glowCX, glowCY, 0, glowCX, glowCY, glowR); + // Glow color: blue normally, red on error + const glowR1 = Math.round(lerp(20, 255, s.error)); + const glowG1 = Math.round(lerp(60, 40, s.error)); + const glowB1 = Math.round(lerp(255, 40, s.error)); + glow.addColorStop(0, `rgba(${glowR1}, ${glowG1}, ${glowB1}, ${glowA * 2})`); + glow.addColorStop( + 0.25, + `rgba(${Math.round(lerp(80, 200, s.error))}, ${Math.round(lerp(140, 50, s.error))}, ${Math.round(lerp(255, 50, s.error))}, ${glowA * 1.2})`, + ); + glow.addColorStop(0.6, `rgba(${glowR1}, ${glowG1}, ${glowB1}, ${glowA * 0.4})`); + glow.addColorStop(1, `rgba(${glowR1}, ${glowG1}, ${glowB1}, 0)`); + ctx.fillStyle = glow; + ctx.beginPath(); + ctx.arc(glowCX, glowCY, glowR, 0, Math.PI * 2); + ctx.fill(); + + // ── Create particles if empty ── + if (particlesRef.current.length === 0) { + particlesRef.current = createParticles(350); + } + + // ── Update & draw particles ── + const cosRY = Math.cos(s.rotY + mouseRotY); + const sinRY = Math.sin(s.rotY + mouseRotY); + const cosRX = Math.cos(mouseRotX); + const sinRX = Math.sin(mouseRotX); + + // Sort by z for correct layering + type ParticleWithScreen = { p: Particle; sx: number; sy: number; sz: number; depth: number }; + const projected: ParticleWithScreen[] = []; + + for (const p of particlesRef.current) { + // Target position: sphere surface + noise displacement + const n = noise(p.theta * p.noiseScale, p.phi * p.noiseScale, time * 0.5 + p.phase); + const displacement = 1 + n * lerp(0.12, 0.3, s.pulse); + + // Orbit: rotate theta — always moving, faster idle + const activeTheta = p.theta + time * p.orbitSpeed * lerp(0.35, 0.8, s.pulse); + + // Sphere coordinates to cartesian + const sinPhi = Math.sin(p.phi); + const tgtX = Math.cos(activeTheta) * sinPhi * displacement; + // Excitement from hover + pulse + error + const targetExcite = Math.max(s.hover * 0.9, s.pulse, s.error * 0.8); + const tgtY = Math.cos(p.phi) * displacement; + const tgtZ = Math.sin(activeTheta) * sinPhi * displacement; + + // Scatter on hover: push particles outward + const scatterMul = 1 + s.scatter * (0.5 + n * 0.5); + + // Spring physics toward target + const tx = tgtX * scatterMul; + const ty = tgtY * scatterMul; + const tz = tgtZ * scatterMul; + + p.vx += (tx - p.x) * 0.08; + p.vy += (ty - p.y) * 0.08; + p.vz += (tz - p.z) * 0.08; + p.vx *= 0.88; + p.vy *= 0.88; + p.vz *= 0.88; + p.x += p.vx; + p.y += p.vy; + p.z += p.vz; + + // 3D rotation (Y then X) + const rx = p.x * cosRY - p.z * sinRY; + const rz = p.x * sinRY + p.z * cosRY; + const ry = p.y * cosRX - rz * sinRX; + const finalZ = p.y * sinRX + rz * cosRX; + + // Project to screen + const perspective = 3; + const scale = perspective / (perspective + finalZ); + const sx = cx + rx * sphereR * scale; + const sy = cy + ry * sphereR * scale; + + projected.push({ p, sx, sy, sz: finalZ, depth: scale }); + } + + // Sort back-to-front + projected.sort((a, b) => a.sz - b.sz); + + for (const { p, sx, sy, sz, depth } of projected) { + // Depth-based alpha and size + const depthAlpha = 0.25 + (sz + 1) * 0.375; // 0.25 (back) → 1.0 (front) + const twinkle = 0.75 + 0.25 * Math.sin(time * 3.5 + p.phase); + + const alpha = + depthAlpha * twinkle * p.brightness * lerp(0.8, 1.3, Math.max(s.pulse, s.hover * 0.8)); + + const drawSize = + p.baseSize * depth * dpr * lerp(1.0, 2.0, Math.max(s.pulse, s.hover * 0.7)); + + // Color — shift to red on error + let r: number, g: number, b: number; + if (s.error > 0.1) { + // Error: red family + if (p.hue === 0) { + r = Math.round(lerp(40 + sz * 30, 255, s.error)); + g = Math.round(lerp(80 + sz * 40, 40 + sz * 20, s.error)); + b = Math.round(lerp(255, 40, s.error)); + } else { + r = Math.round(lerp(100 + sz * 30, 230, s.error)); + g = Math.round(lerp(220 + sz * 17, 60, s.error)); + b = Math.round(lerp(20, 20, s.error)); + } + } else if (p.hue === 0) { + r = 60 + Math.round(sz * 40); + g = 100 + Math.round(sz * 50); + b = 255; + } else { + r = 120 + Math.round(sz * 30); + g = 237 + Math.round(sz * 10); + b = 30; + } + + // Thinking: shift toward brighter, more saturated + if (s.pulse > 0.1) { + r = Math.round(lerp(r, p.hue === 0 ? 100 : 130, s.pulse * 0.3)); + g = Math.round(lerp(g, p.hue === 0 ? 140 : 237, s.pulse * 0.3)); + b = Math.round(lerp(b, p.hue === 0 ? 255 : 32, s.pulse * 0.3)); + } + + // Micro glow — always visible, stronger on front + if (depthAlpha > 0.25) { + const gSize = drawSize * lerp(4, 7, s.hover); + const pg = ctx.createRadialGradient(sx, sy, 0, sx, sy, gSize); + pg.addColorStop(0, `rgba(${r},${g},${b},${alpha * 0.5})`); + pg.addColorStop(1, `rgba(${r},${g},${b},0)`); + ctx.fillStyle = pg; + ctx.beginPath(); + ctx.arc(sx, sy, gSize, 0, Math.PI * 2); + ctx.fill(); + } + + // Core dot — bright + ctx.fillStyle = `rgba(${Math.min(r + 40, 255)},${Math.min(g + 30, 255)},${b},${Math.min(alpha * 1.6, 1)})`; + ctx.beginPath(); + ctx.arc(sx, sy, Math.max(drawSize * 0.5, 0.3 * dpr), 0, Math.PI * 2); + ctx.fill(); + } + + // ── Loading rings (thinking) ── + if (s.pulse > 0.02) { + ctx.save(); + ctx.translate(cx, cy); + + // Spinning arc + const spinAngle = time * 2; + const arcLen = Math.PI * lerp(0.3, 1.0, (Math.sin(time * 1.5) + 1) / 2); + ctx.rotate(spinAngle); + ctx.strokeStyle = `rgba(130, 237, 32, ${s.pulse * 0.4})`; + ctx.lineWidth = 1.2 * dpr; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.arc(0, 0, sphereR * 1.25, 0, arcLen); + ctx.stroke(); + + // Counter-spinning arc + ctx.rotate(-spinAngle * 2); + ctx.strokeStyle = `rgba(1, 29, 255, ${s.pulse * 0.3})`; + ctx.lineWidth = 0.8 * dpr; + ctx.beginPath(); + ctx.arc(0, 0, sphereR * 1.35, 0, arcLen * 0.6); + ctx.stroke(); + + ctx.restore(); + + // Expanding pulse + const pulsePhase = (time * 0.8) % 1; + const pulseR = sphereR * (1 + pulsePhase * 1.5); + const pulseA = s.pulse * (1 - pulsePhase) * 0.15; + ctx.strokeStyle = `rgba(130, 237, 32, ${pulseA})`; + ctx.lineWidth = 1 * dpr; + ctx.beginPath(); + ctx.arc(cx, cy, pulseR, 0, Math.PI * 2); + ctx.stroke(); + } + + animRef.current = requestAnimationFrame(drawStep); + }, + [isThinking, hasError], + ); + + useEffect(() => { + animRef.current = requestAnimationFrame(draw); + return () => cancelAnimationFrame(animRef.current); + }, [draw]); + + return ( +
+ +
+ ); +} diff --git a/components/search/AISearchResults.tsx b/components/search/AISearchResults.tsx new file mode 100644 index 00000000..6db83223 --- /dev/null +++ b/components/search/AISearchResults.tsx @@ -0,0 +1,646 @@ +'use client'; + +import { useState, useRef, useEffect, KeyboardEvent } from 'react'; +import { ArrowUp, X, Sparkles, ChevronRight, RotateCcw, Copy, Check } from 'lucide-react'; +import Link from 'next/link'; +import { useAnalytics } from '../analytics/useAnalytics'; +import { AnalyticsEvents } from '../analytics/analytics-events'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import dynamic from 'next/dynamic'; +const AIOrb = dynamic(() => import('./AIOrb'), { ssr: false }); + +const LOADING_TEXTS = [ + 'Durchsuche das Kabelhandbuch... 📖', + 'Frage den Senior-Ingenieur... 👴🔧', + 'Frage ChatGPTs Cousin 2. Grades... 🤖', +]; + +interface ProductMatch { + id: string; + title: string; + sku: string; + slug: string; +} + +interface Message { + role: 'user' | 'assistant'; + content: string; + products?: ProductMatch[]; + timestamp: number; +} +interface ComponentProps { + isOpen: boolean; + onClose: () => void; + initialQuery?: string; + triggerSearch?: boolean; +} + +export function AISearchResults({ + isOpen, + onClose, + initialQuery = '', + triggerSearch = false, +}: ComponentProps) { + const { trackEvent } = useAnalytics(); + + const [query, setQuery] = useState(''); + const [messages, setMessages] = useState([]); + const [honeypot, setHoneypot] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [copiedIndex, setCopiedIndex] = useState(null); + const [copiedAll, setCopiedAll] = useState(false); + const [loadingText, setLoadingText] = useState(LOADING_TEXTS[0]); + + const inputRef = useRef(null); + const modalRef = useRef(null); + const messagesEndRef = useRef(null); + const loadingIntervalRef = useRef | null>(null); + const hasTriggeredRef = useRef(false); + + // Dedicated focus effect — polls until the input actually has focus + useEffect(() => { + if (!isOpen) return; + let attempts = 0; + const focusTimer = setInterval(() => { + const el = inputRef.current; + if (el && document.activeElement !== el) { + el.focus({ preventScroll: true }); + } + attempts++; + if (attempts >= 15 || document.activeElement === el) { + clearInterval(focusTimer); + } + }, 100); + return () => clearInterval(focusTimer); + }, [isOpen]); + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + + // Trigger initial search only once + if (triggerSearch && initialQuery && !hasTriggeredRef.current) { + hasTriggeredRef.current = true; + handleSearch(initialQuery); + } + } else { + document.body.style.overflow = 'unset'; + setQuery(''); + setMessages([]); + setError(null); + setIsLoading(false); + hasTriggeredRef.current = false; + } + return () => { + document.body.style.overflow = 'unset'; + }; + }, [isOpen, triggerSearch]); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, isLoading]); + + // Global ESC handler + useEffect(() => { + if (!isOpen) return; + const handleEsc = (e: globalThis.KeyboardEvent) => { + if (e.key === 'Escape') { + const activeElement = document.activeElement; + const isInputFocused = activeElement === inputRef.current; + + if (query.trim()) { + // If there's text, clear it but keep chat open + setQuery(''); + inputRef.current?.focus(); + } else if (!isInputFocused) { + // If no text and input is not focused, focus it + inputRef.current?.focus(); + } else { + // If no text and input IS focused, close the chat + onClose(); + } + } + }; + document.addEventListener('keydown', handleEsc); + return () => document.removeEventListener('keydown', handleEsc); + }, [isOpen, onClose, query]); + + const handleSearch = async (searchQuery: string = query) => { + if (!searchQuery.trim() || isLoading) return; + + const newUserMessage: Message = { role: 'user', content: searchQuery, timestamp: Date.now() }; + const newMessagesContext = [...messages, newUserMessage]; + + setMessages(newMessagesContext); + setQuery(''); // Always clear input after send + setError(null); + + // Give the user message animation 400ms to arrive before showing "thinking" + setTimeout(() => { + setIsLoading(true); + // Start rotating loading texts + let textIdx = Math.floor(Math.random() * LOADING_TEXTS.length); + setLoadingText(LOADING_TEXTS[textIdx]); + loadingIntervalRef.current = setInterval(() => { + textIdx = (textIdx + 1) % LOADING_TEXTS.length; + setLoadingText(LOADING_TEXTS[textIdx]); + }, 2500); + }, 400); + + trackEvent(AnalyticsEvents.FORM_SUBMIT, { + type: 'ai_chat', + query: searchQuery, + }); + + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 60000); + + const res = await fetch('/api/ai-search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + signal: controller.signal, + body: JSON.stringify({ + messages: newMessagesContext, + _honeypot: honeypot, + }), + }); + + clearTimeout(timeout); + + const data = await res.json().catch(() => null); + + if (!res.ok || !data) { + throw new Error(data?.error || `Server antwortete mit Status ${res.status}`); + } + + setMessages((prev) => [ + ...prev, + { + role: 'assistant', + content: data.answerText, + products: data.products, + timestamp: Date.now(), + }, + ]); + + setTimeout(() => inputRef.current?.focus(), 100); + } catch (err: any) { + console.error(err); + const msg = + err.name === 'AbortError' + ? 'Anfrage hat zu lange gedauert. Bitte versuche es erneut.' + : err.message || 'Ein Fehler ist aufgetreten.'; + + // Show error as a system message in the chat instead of a separate error banner + setMessages((prev) => [ + ...prev, + { + role: 'assistant', + content: `⚠️ ${msg}`, + timestamp: Date.now(), + }, + ]); + + trackEvent(AnalyticsEvents.ERROR, { + location: 'ai_chat', + message: err.message, + query: searchQuery, + }); + } finally { + setIsLoading(false); + if (loadingIntervalRef.current) { + clearInterval(loadingIntervalRef.current); + loadingIntervalRef.current = null; + } + // Always re-focus the input + setTimeout(() => inputRef.current?.focus(), 50); + } + }; + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSearch(); + } + if (e.key === 'ArrowUp' && !query) { + // Find the last user message and put it into the input + const lastUserNav = [...messages].reverse().find((m) => m.role === 'user'); + if (lastUserNav) { + e.preventDefault(); + setQuery(lastUserNav.content); + } + } + }; + + const handleCopy = (content: string, index?: number) => { + navigator.clipboard.writeText(content); + if (index !== undefined) { + setCopiedIndex(index); + setTimeout(() => setCopiedIndex(null), 2000); + } else { + setCopiedAll(true); + setTimeout(() => setCopiedAll(false), 2000); + } + }; + + const handleCopyChat = () => { + const fullChat = messages + .map((m) => `${m.role === 'user' ? 'Du' : 'Ohm'}:\n${m.content}`) + .join('\n\n'); + handleCopy(fullChat); + }; + + if (!isOpen) return null; + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + return ( +
+ {/* Animated backdrop */} +
+ +
+ {/* ── Glassmorphism container ── */} +
+ {/* ── Header ── */} +
+
+
+ +
+
+

Ohm

+

+ {isLoading ? 'Denkt nach...' : error ? 'Fehler aufgetreten' : 'Online'} +

+
+
+
+ {messages.length > 0 && ( + + )} + +
+
+ + {/* ── Chat Area ── */} +
+ {/* Empty state */} + {messages.length === 0 && !isLoading && !error && ( +
+
+ +
+
+

+ Wie kann ich helfen? +

+

+ Beschreibe dein Projekt, frag nach bestimmten Kabeln, oder nenne mir deine + Anforderungen. +

+
+ {/* Quick prompts */} +
+ {['Windpark 33kV Verkabelung', 'NYCWY 4x185', 'Erdkabel für Solarpark'].map( + (prompt) => ( + + ), + )} +
+
+ )} + + {/* Messages */} + {messages.map((msg, index) => ( +
+
+ {/* Copy Button */} + + + {msg.role === 'assistant' && ( +
+ + + Ohm + +
+ )} +
+ {msg.role === 'assistant' ? ( + {msg.content} + ) : ( +

{msg.content}

+ )} +
+ + {/* Timestamp */} + {!msg.products?.length && ( +

+ {new Date(msg.timestamp).toLocaleTimeString('de', { + hour: '2-digit', + minute: '2-digit', + })} +

+ )} + + {/* Product cards */} + {msg.role === 'assistant' && msg.products && msg.products.length > 0 && ( +
+

+ Empfohlene Produkte +

+
+ {msg.products.map((product, idx) => ( + { + onClose(); + trackEvent(AnalyticsEvents.BUTTON_CLICK, { + target: product.slug, + location: 'ai_chat', + }); + }} + className="group flex items-center justify-between bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.06] hover:border-accent/30 rounded-xl px-4 py-3 transition-all duration-300" + style={{ animation: `chatFadeIn 0.3s ease-out ${idx * 0.1}s both` }} + > +
+

+ {product.sku} +

+
+ {product.title} +
+
+ + + ))} +
+
+ )} +
+
+ ))} + + {/* Loading indicator */} + {isLoading && ( +
+
+
+ +
+
+

+ {loadingText} +

+
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+
+
+
+ )} + + {/* Error */} + {error && ( +
+
+
+ +
+
+

Da ist was schiefgelaufen 😬

+

{error}

+ +
+
+
+ )} + +
+
+ + {/* ── Input Area ── */} +
+
+ setQuery(e.target.value)} + onKeyDown={onKeyDown} + placeholder="Nachricht eingeben..." + className="flex-1 bg-transparent border-none text-white text-sm md:text-base px-5 py-4 focus:outline-none placeholder:text-white/20" + disabled={isLoading} + tabIndex={1} + autoFocus + /> + setHoneypot(e.target.value)} + tabIndex={-1} + autoComplete="off" + aria-hidden="true" + /> + +
+
+ + Enter zum Senden · Esc zum Schließen + + + · + + + 🛡️ DSGVO-konform · EU-Server + +
+
+
+
+ + {/* ── Keyframe animations ── */} + +
+ ); +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2d504f82..2134214e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -8,7 +8,7 @@ services: - infra labels: - "caddy=http://${TRAEFIK_HOST:-klz.localhost}" - - "caddy.reverse_proxy=host.docker.internal:3100" + - "caddy.reverse_proxy=http://klz-app:3000" # Full Docker dev (use with `pnpm run dev:docker`) klz-app: @@ -26,13 +26,20 @@ services: - ${ENV_FILE:-.env} environment: NODE_ENV: development + # Force Garbage Collection before Docker kills the container (OOM) + NODE_OPTIONS: "--max-old-space-size=6144" NEXT_TELEMETRY_DISABLED: "1" POSTGRES_URI: postgres://${PAYLOAD_DB_USER:-payload}:${PAYLOAD_DB_PASSWORD:-120in09oenaoinsd9iaidon}@klz-db:5432/${PAYLOAD_DB_NAME:-payload} PAYLOAD_SECRET: ${PAYLOAD_SECRET:-fallback-secret-for-dev} - NODE_OPTIONS: "--max-old-space-size=8192" - UV_THREADPOOL_SIZE: "4" + UV_THREADPOOL_SIZE: "1" + RAYON_NUM_THREADS: "1" + NEXT_PRIVATE_WORKER_THREADS: "false" NPM_TOKEN: ${NPM_TOKEN:-} CI: "true" + QDRANT_URL: "http://klz-qdrant:6333" + REDIS_URL: "redis://klz-redis:6379" + MISTRAL_API_KEY: ${MISTRAL_API_KEY:-} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} volumes: - .:/app - klz_node_modules:/app/node_modules @@ -42,19 +49,34 @@ services: - /app/.git - /app/reference - /app/data + deploy: resources: limits: - cpus: '4' memory: 8G command: > - sh -c "pnpm install && pnpm next dev --webpack --hostname 0.0.0.0" + sh -c "pnpm install --no-frozen-lockfile && + while true; do + ( + echo '[warmup] Waiting for Next.js to be reachable...' + until curl -sf http://localhost:3000 > /dev/null; do sleep 2; done + echo '[warmup] Server is up! Pre-compiling routes...' + curl -sf http://localhost:3000/de > /dev/null 2>&1 && echo '[warmup] /de ready' + curl -sf http://localhost:3000/api/health/cms > /dev/null 2>&1 && echo '[warmup] /api/health/cms ready' + curl -sf -X POST -H 'Content-Type: application/json' -d '{\"messages\":[{\"role\":\"user\",\"content\":\"warmup\"}]}' http://localhost:3000/api/ai-search > /dev/null 2>&1 && echo '[warmup] /api/ai-search ready' + echo '[warmup] Syncing CMS data to Qdrant...' + SYNC_RESULT=$(curl -sf http://localhost:3000/api/sync-qdrant 2>&1) + echo \"[warmup] Qdrant sync: $SYNC_RESULT\" + echo '[warmup] All routes pre-compiled + Qdrant synced ✓' + ) & + pnpm next dev --webpack --hostname 0.0.0.0; + echo '[klz-app] next dev exited, restarting in 2s...'; + sleep 2; + done" labels: - "traefik.enable=true" - "traefik.http.services.${PROJECT_NAME:-klz}-app-svc.loadbalancer.server.port=3000" - "traefik.docker.network=infra" - - "caddy=http://${TRAEFIK_HOST:-klz.localhost}" - - "caddy.reverse_proxy={{upstreams 3000}}" klz-db: image: postgres:15-alpine @@ -75,6 +97,35 @@ services: ports: - "54322:5432" + klz-redis: + image: redis:7-alpine + restart: unless-stopped + networks: + - default + ports: + - "16379:6379" + + klz-qdrant: + image: qdrant/qdrant:v1.13.2 + restart: unless-stopped + volumes: + - klz_qdrant_data:/qdrant/storage + networks: + - default + - "16333:6333" + + klz-kabelfachmann: + image: registry.infra.mintel.me/mintel/kabelfachmann-mcp:latest + restart: unless-stopped + networks: + - default + env_file: + - ${ENV_FILE:-.env} + environment: + QDRANT_URL: http://klz-qdrant:6333 + depends_on: + - klz-qdrant + networks: default: name: ${PROJECT_NAME:-klz-cables}-internal @@ -84,6 +135,8 @@ networks: volumes: klz_db_data: external: false + klz_qdrant_data: + external: false klz_node_modules: klz_next_cache: klz_turbo_cache: diff --git a/docker-compose.yml b/docker-compose.yml index e2f388a5..06379298 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,6 +100,37 @@ services: networks: - default + klz-redis: + image: redis:7-alpine + restart: unless-stopped + networks: + - default + + klz-qdrant: + image: qdrant/qdrant:v1.13.2 + restart: unless-stopped + ports: + - "6333:6333" + environment: + QDRANT__SERVICE__HTTP_PORT: 6333 + QDRANT__SERVICE__GRPC_PORT: 6334 + volumes: + - klz_qdrant_data:/qdrant/storage + networks: + - default + + klz-kabelfachmann: + image: registry.infra.mintel.me/mintel/kabelfachmann-mcp:latest + restart: unless-stopped + networks: + - default + env_file: + - ${ENV_FILE:-.env} + environment: + QDRANT_URL: http://klz-qdrant:6333 + depends_on: + - klz-qdrant + networks: default: name: ${PROJECT_NAME:-klz-cables}-internal @@ -111,3 +142,5 @@ volumes: external: false klz_media_data: external: false + klz_qdrant_data: + external: false diff --git a/eslint.config.mjs b/eslint.config.mjs index 9bd59e89..28349226 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -23,7 +23,9 @@ export default [ "tests/**", "next-env.d.ts", "reference/**", - "data/**" + "data/**", + "remotion/**", + "components/record-mode/**" ], }, diff --git a/next.config.mjs b/next.config.mjs index 103c4165..5296df46 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -7,17 +7,28 @@ import { withPayload } from '@payloadcms/next/withPayload'; const isProd = process.env.NODE_ENV === 'production'; /** @type {import('next').NextConfig} */ const nextConfig = { + transpilePackages: ['react-image-crop', '@react-three/fiber'], onDemandEntries: { - // Make sure entries are not disposed too quickly - maxInactiveAge: 60 * 1000, + // Keep compiled pages/routes in memory for 5 minutes (reduced from 25m to prevent OOM) + maxInactiveAge: 5 * 60 * 1000, + // Keep up to 2 pages in the dev buffer (reduced from 10 to prevent OOM) + pagesBufferLength: 2, }, experimental: { - optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'], - cpus: 3, + optimizePackageImports: [ + 'lucide-react', + 'framer-motion', + '@/components/ui', + '@sentry/nextjs', + '@payloadcms/richtext-lexical', + 'react-hook-form', + 'zod', + 'date-fns', + ], workerThreads: false, + memoryBasedWorkersCount: true, }, reactStrictMode: false, - swcMinify: true, productionBrowserSourceMaps: false, logging: { fetches: { @@ -25,6 +36,21 @@ const nextConfig = { }, }, ...(isProd ? { output: 'standalone' } : {}), + // Prevent webpack from restarting when .env files are touched via Docker volume mount + webpack: (config, { dev }) => { + if (dev) { + config.watchOptions = { + ...config.watchOptions, + ignored: /node_modules|\.env/, + // Reduce poll frequency to lower CPU churn from VirtioFS + poll: 1000, + aggregateTimeout: 300, + }; + // Reduce source map quality in dev for faster rebuilds + config.devtool = 'eval'; + } + return config; + }, async headers() { const isProd = process.env.NODE_ENV === 'production'; const umamiDomain = new URL(process.env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me').origin; @@ -47,7 +73,7 @@ const nextConfig = { style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: ${extraImgDomains}; - connect-src 'self' ${umamiDomain} ${glitchtipDomain}; + connect-src 'self' ${umamiDomain} ${glitchtipDomain} https://raw.githack.com https://raw.githubusercontent.com; frame-src 'self'; object-src 'none'; base-uri 'self'; @@ -393,6 +419,7 @@ const nextConfig = { ]; }, images: { + qualities: [25, 50, 75, 100], formats: ['image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], remotePatterns: [ diff --git a/package.json b/package.json index 15c33a9b..031f28bc 100644 --- a/package.json +++ b/package.json @@ -4,19 +4,26 @@ "private": true, "packageManager": "pnpm@10.18.3", "dependencies": { - "@mintel/mail": "^1.8.21", - "@mintel/next-config": "^1.8.21", - "@mintel/next-feedback": "^1.8.21", - "@mintel/next-utils": "^1.8.21", + "@ai-sdk/google": "^3.0.31", + "@ai-sdk/openai": "^3.0.36", + "@mintel/mail": "^1.9.0", + "@mintel/next-config": "^1.9.0", + "@mintel/next-feedback": "^1.9.0", + "@mintel/next-utils": "^1.9.0", + "@mintel/payload-ai": "^1.9.15", "@payloadcms/db-postgres": "^3.77.0", "@payloadcms/email-nodemailer": "^3.77.0", "@payloadcms/next": "^3.77.0", "@payloadcms/richtext-lexical": "^3.77.0", "@payloadcms/ui": "^3.77.0", + "@qdrant/js-client-rest": "^1.17.0", "@react-email/components": "^1.0.7", "@react-pdf/renderer": "^4.3.2", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", "@sentry/nextjs": "^10.39.0", "@types/recharts": "^2.0.1", + "ai": "^6.0.101", "axios": "^1.13.5", "clsx": "^2.1.1", "framer-motion": "^12.34.0", @@ -24,6 +31,7 @@ "gray-matter": "^4.0.3", "i18next": "^25.7.3", "import-in-the-middle": "^1.11.0", + "ioredis": "^5.9.3", "jsdom": "^27.4.0", "leaflet": "^1.9.4", "next": "16.1.6", @@ -38,13 +46,17 @@ "react-dom": "^19.2.4", "react-email": "^5.2.5", "react-leaflet": "^4.2.1", + "react-markdown": "^10.1.0", "recharts": "^3.7.0", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", "require-in-the-middle": "^8.0.1", "resend": "^3.5.0", "schema-dts": "^1.1.5", "sharp": "^0.34.5", "svg-to-pdfkit": "^0.1.8", "tailwind-merge": "^3.4.0", + "three": "^0.183.1", "xlsx": "npm:@e965/xlsx@^0.20.3", "zod": "3.25.76" }, @@ -53,8 +65,8 @@ "@commitlint/config-conventional": "^20.4.0", "@cspell/dict-de-de": "^4.1.2", "@lhci/cli": "^0.15.1", - "@mintel/eslint-config": "1.8.21", - "@mintel/tsconfig": "^1.8.21", + "@mintel/eslint-config": "^1.9.0", + "@mintel/tsconfig": "^1.9.0", "@next/bundle-analyzer": "^16.1.6", "@tailwindcss/cli": "^4.1.18", "@tailwindcss/postcss": "^4.1.18", @@ -80,6 +92,7 @@ "lint-staged": "^16.2.7", "lucide-react": "^0.563.0", "pa11y-ci": "^4.0.1", + "pdf-parse": "^2.4.5", "postcss": "^8.5.6", "prettier": "^3.8.1", "puppeteer": "^24.37.3", @@ -92,8 +105,8 @@ "vitest": "^4.0.16" }, "scripts": { - "dev": "bash -c 'trap \"COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up klz-app klz-db --remove-orphans'", - "dev:local": "bash -c 'trap \"COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up -d klz-db klz-proxy && POSTGRES_URI=NODE_ENV=development next dev --webpack --port 3100 --hostname 0.0.0.0'", + "dev": "bash -c '[ -f .env ] || (cp .env.example .env && sed -i.bak \"s/TRAEFIK_HOST=klz-cables.com/TRAEFIK_HOST=klz.localhost/\" .env && rm -f .env.bak && echo \"✅ Created .env from .env.example\"); trap \"COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up klz-app klz-db klz-proxy klz-qdrant klz-redis --remove-orphans'", + "dev:local": "bash -c 'trap \"COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up -d klz-db klz-proxy klz-qdrant klz-redis && POSTGRES_URI=NODE_ENV=development next dev --webpack --port 3100 --hostname 0.0.0.0'", "dev:infra": "COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up -d klz-db klz-proxy", "build": "next build", "start": "next start", @@ -125,6 +138,10 @@ "assets:pull:prod": "bash ./scripts/assets-sync.sh prod local", "assets:sync:testing-to-staging": "bash ./scripts/assets-sync.sh testing staging", "assets:sync:staging-to-prod": "bash ./scripts/assets-sync.sh staging prod", + "qdrant:push:testing": "bash ./scripts/qdrant-sync.sh testing", + "qdrant:push:staging": "bash ./scripts/qdrant-sync.sh staging", + "qdrant:push:prod": "bash ./scripts/qdrant-sync.sh prod", + "qdrant:push:branch": "bash ./scripts/qdrant-sync.sh", "pagespeed:test": "tsx ./scripts/pagespeed-sitemap.ts", "pagespeed:audit": "./scripts/audit-local.sh", "pagespeed:urls": "tsx -e \"import sitemap from './app/sitemap'; sitemap().then(urls => console.log(urls.map(u => u.url).join('\\n')))\"", @@ -151,6 +168,9 @@ "overrides": { "next": "16.1.6", "minimatch": ">=10.2.2" + }, + "patchedDependencies": { + "@mintel/payload-ai@1.9.15": "patches/@mintel__payload-ai@1.9.15.patch" } }, "browserslist": [ diff --git a/patches/@mintel__payload-ai@1.9.15.patch b/patches/@mintel__payload-ai@1.9.15.patch new file mode 100644 index 00000000..3593550a --- /dev/null +++ b/patches/@mintel__payload-ai@1.9.15.patch @@ -0,0 +1,131 @@ +diff --git a/dist/components/ChatWindow/index.js b/dist/components/ChatWindow/index.js +index 90c65bae4abb78beec98d8308e808e8ba341dcc2..f675dbc69ff82b64438288f53599c93a56391b64 100644 +--- a/dist/components/ChatWindow/index.js ++++ b/dist/components/ChatWindow/index.js +@@ -2,7 +2,6 @@ + import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; + import { useState } from 'react'; + import { useChat } from '@ai-sdk/react'; +-import './ChatWindow.scss'; + export const ChatWindowProvider = ({ children }) => { + return (_jsxs(_Fragment, { children: [children, _jsx(ChatWindow, {})] })); + }; +@@ -14,47 +13,63 @@ const ChatWindow = () => { + initialMessages: [] + }); + // Basic implementation to toggle chat window and submit messages +- return (_jsxs("div", { className: "payload-mcp-chat-container", children: [_jsx("button", { className: "payload-mcp-chat-toggle", onClick: () => setIsOpen(!isOpen), style: { +- position: 'fixed', +- bottom: '20px', +- right: '20px', +- zIndex: 9999, +- padding: '12px 24px', +- backgroundColor: '#000', +- color: '#fff', +- borderRadius: '8px', +- border: 'none', +- cursor: 'pointer', +- fontWeight: 'bold' +- }, children: isOpen ? 'Close AI Chat' : 'Ask AI' }), isOpen && (_jsxs("div", { className: "payload-mcp-chat-window", style: { +- position: 'fixed', +- bottom: '80px', +- right: '20px', +- width: '400px', +- height: '600px', +- backgroundColor: '#fff', +- border: '1px solid #eaeaea', +- borderRadius: '12px', +- zIndex: 9999, +- display: 'flex', +- flexDirection: 'column', +- boxShadow: '0 10px 40px rgba(0,0,0,0.1)' +- }, children: [_jsx("div", { className: "chat-header", style: { padding: '16px', borderBottom: '1px solid #eaeaea', backgroundColor: '#f9f9f9', borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }, children: _jsx("h3", { style: { margin: 0, fontSize: '16px' }, children: "Payload MCP Chat" }) }), _jsx("div", { className: "chat-messages", style: { flex: 1, padding: '16px', overflowY: 'auto' }, children: messages.map((m) => (_jsx("div", { style: { +- marginBottom: '12px', +- textAlign: m.role === 'user' ? 'right' : 'left' +- }, children: _jsxs("div", { style: { +- display: 'inline-block', +- padding: '8px 12px', +- borderRadius: '8px', +- backgroundColor: m.role === 'user' ? '#000' : '#f0f0f0', +- color: m.role === 'user' ? '#fff' : '#000', +- maxWidth: '80%' +- }, children: [m.role === 'user' ? 'G: ' : 'AI: ', m.content] }) }, m.id))) }), _jsx("form", { onSubmit: handleSubmit, style: { padding: '16px', borderTop: '1px solid #eaeaea' }, children: _jsx("input", { value: input, placeholder: "Ask me anything or use /commands...", onChange: handleInputChange, style: { +- width: '100%', +- padding: '12px', +- borderRadius: '8px', +- border: '1px solid #eaeaea', +- boxSizing: 'border-box' +- } }) })] }))] })); ++ return (_jsxs("div", { ++ className: "payload-mcp-chat-container", children: [_jsx("button", { ++ className: "payload-mcp-chat-toggle", onClick: () => setIsOpen(!isOpen), style: { ++ position: 'fixed', ++ bottom: '20px', ++ right: '20px', ++ zIndex: 9999, ++ padding: '12px 24px', ++ backgroundColor: '#000', ++ color: '#fff', ++ borderRadius: '8px', ++ border: 'none', ++ cursor: 'pointer', ++ fontWeight: 'bold' ++ }, children: isOpen ? 'Close AI Chat' : 'Ask AI' ++ }), isOpen && (_jsxs("div", { ++ className: "payload-mcp-chat-window", style: { ++ position: 'fixed', ++ bottom: '80px', ++ right: '20px', ++ width: '400px', ++ height: '600px', ++ backgroundColor: '#fff', ++ border: '1px solid #eaeaea', ++ borderRadius: '12px', ++ zIndex: 9999, ++ display: 'flex', ++ flexDirection: 'column', ++ boxShadow: '0 10px 40px rgba(0,0,0,0.1)' ++ }, children: [_jsx("div", { className: "chat-header", style: { padding: '16px', borderBottom: '1px solid #eaeaea', backgroundColor: '#f9f9f9', borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }, children: _jsx("h3", { style: { margin: 0, fontSize: '16px' }, children: "Payload MCP Chat" }) }), _jsx("div", { ++ className: "chat-messages", style: { flex: 1, padding: '16px', overflowY: 'auto' }, children: messages.map((m) => (_jsx("div", { ++ style: { ++ marginBottom: '12px', ++ textAlign: m.role === 'user' ? 'right' : 'left' ++ }, children: _jsxs("div", { ++ style: { ++ display: 'inline-block', ++ padding: '8px 12px', ++ borderRadius: '8px', ++ backgroundColor: m.role === 'user' ? '#000' : '#f0f0f0', ++ color: m.role === 'user' ? '#fff' : '#000', ++ maxWidth: '80%' ++ }, children: [m.role === 'user' ? 'G: ' : 'AI: ', m.content] ++ }) ++ }, m.id))) ++ }), _jsx("form", { ++ onSubmit: handleSubmit, style: { padding: '16px', borderTop: '1px solid #eaeaea' }, children: _jsx("input", { ++ value: input, placeholder: "Ask me anything or use /commands...", onChange: handleInputChange, style: { ++ width: '100%', ++ padding: '12px', ++ borderRadius: '8px', ++ border: '1px solid #eaeaea', ++ boxSizing: 'border-box' ++ } ++ }) ++ })] ++ }))] ++ })); + }; + //# sourceMappingURL=index.js.map +\ No newline at end of file +diff --git a/src/components/ChatWindow/index.tsx b/src/components/ChatWindow/index.tsx +index 9081ae77d4eae53ce660e285c1a6babde99ceaab..f262f1dd0fd1199734024cc27905d956e31900a2 100644 +--- a/src/components/ChatWindow/index.tsx ++++ b/src/components/ChatWindow/index.tsx +@@ -2,7 +2,6 @@ + + import React, { useState } from 'react' + import { useChat } from '@ai-sdk/react' +-import './ChatWindow.scss' + + export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( diff --git a/payload.config.ts b/payload.config.ts index 289ba29c..23de3294 100644 --- a/payload.config.ts +++ b/payload.config.ts @@ -23,6 +23,13 @@ import { Products } from './src/payload/collections/Products'; import { Pages } from './src/payload/collections/Pages'; import { seedDatabase } from './src/payload/seed'; +const isMigrate = process.argv.includes('migrate'); +let chatPlugin: any = null; +if (!isMigrate) { + const mod = await import('@mintel/payload-ai'); + chatPlugin = mod.payloadChatPlugin; +} + const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); @@ -98,5 +105,14 @@ export default buildConfig({ }) : undefined, sharp, - plugins: [], + plugins: [ + ...(chatPlugin + ? [ + chatPlugin({ + enabled: true, + mcpServers: [{ name: 'klz-qdrant-mcp' }], + }), + ] + : []), + ], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39b47d21..ed77caea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,22 +8,36 @@ overrides: next: 16.1.6 minimatch: '>=10.2.2' +patchedDependencies: + '@mintel/payload-ai@1.9.15': + hash: 934315d3f15180552789a94bf76c34624501f601cc7409c24da3dc27af96c8fb + path: patches/@mintel__payload-ai@1.9.15.patch + importers: .: dependencies: + '@ai-sdk/google': + specifier: ^3.0.31 + version: 3.0.31(zod@3.25.76) + '@ai-sdk/openai': + specifier: ^3.0.36 + version: 3.0.36(zod@3.25.76) '@mintel/mail': - specifier: ^1.8.21 - version: 1.8.21(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^1.9.0 + version: 1.9.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@mintel/next-config': - specifier: ^1.8.21 - version: 1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0(esbuild@0.25.12)) + specifier: ^1.9.0 + version: 1.9.5(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0(esbuild@0.25.12)) '@mintel/next-feedback': - specifier: ^1.8.21 - version: 1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) + specifier: ^1.9.0 + version: 1.9.5(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) '@mintel/next-utils': - specifier: ^1.8.21 - version: 1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3) + specifier: ^1.9.0 + version: 1.9.5(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3) + '@mintel/payload-ai': + specifier: ^1.9.15 + version: 1.9.15(patch_hash=934315d3f15180552789a94bf76c34624501f601cc7409c24da3dc27af96c8fb)(@payloadcms/next@3.77.0(@types/react@19.2.13)(graphql@16.12.0)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(@payloadcms/ui@3.77.0(@types/react@19.2.13)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(ws@8.19.0) '@payloadcms/db-postgres': specifier: ^3.77.0 version: 3.77.0(@opentelemetry/api@1.9.0)(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3)) @@ -39,18 +53,30 @@ importers: '@payloadcms/ui': specifier: ^3.77.0 version: 3.77.0(@types/react@19.2.13)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + '@qdrant/js-client-rest': + specifier: ^1.17.0 + version: 1.17.0(typescript@5.9.3) '@react-email/components': specifier: ^1.0.7 version: 1.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-pdf/renderer': specifier: ^4.3.2 version: 4.3.2(react@19.2.4) + '@react-three/drei': + specifier: ^10.7.7 + version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.13)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.1))(@types/react@19.2.13)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.1) + '@react-three/fiber': + specifier: ^9.5.0 + version: 9.5.0(@types/react@19.2.13)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.1) '@sentry/nextjs': specifier: ^10.39.0 version: 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.0(esbuild@0.25.12)) '@types/recharts': specifier: ^2.0.1 version: 2.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4)(redux@5.0.1) + ai: + specifier: ^6.0.101 + version: 6.0.101(zod@3.25.76) axios: specifier: ^1.13.5 version: 1.13.5(debug@4.4.3) @@ -72,6 +98,9 @@ importers: import-in-the-middle: specifier: ^1.11.0 version: 1.15.0 + ioredis: + specifier: ^5.9.3 + version: 5.9.3 jsdom: specifier: ^27.4.0 version: 27.4.0 @@ -114,9 +143,18 @@ importers: react-leaflet: specifier: ^4.2.1 version: 4.2.1(leaflet@1.9.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.13)(react@19.2.4) recharts: specifier: ^3.7.0 version: 3.7.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4)(redux@5.0.1) + rehype-raw: + specifier: ^7.0.0 + version: 7.0.0 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 require-in-the-middle: specifier: ^8.0.1 version: 8.0.1 @@ -135,6 +173,9 @@ importers: tailwind-merge: specifier: ^3.4.0 version: 3.4.0 + three: + specifier: ^0.183.1 + version: 0.183.1 xlsx: specifier: npm:@e965/xlsx@^0.20.3 version: '@e965/xlsx@0.20.3' @@ -155,11 +196,11 @@ importers: specifier: ^0.15.1 version: 0.15.1 '@mintel/eslint-config': - specifier: 1.8.21 - version: 1.8.21(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + specifier: ^1.9.0 + version: 1.9.5(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@mintel/tsconfig': - specifier: ^1.8.21 - version: 1.8.21 + specifier: ^1.9.0 + version: 1.9.5 '@next/bundle-analyzer': specifier: ^16.1.6 version: 16.1.6 @@ -235,6 +276,9 @@ importers: pa11y-ci: specifier: ^4.0.1 version: 4.0.1(typescript@5.9.3) + pdf-parse: + specifier: ^2.4.5 + version: 2.4.5 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -271,6 +315,58 @@ packages: '@acemir/cssom@0.9.31': resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + '@ai-sdk/gateway@3.0.55': + resolution: {integrity: sha512-7xMeTJnCjwRwXKVCiv4Ly4qzWvDuW3+W1WIV0X1EFu6W83d4mEhV9bFArto10MeTw40ewuDjrbrZd21mXKohkw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/gateway@3.0.66': + resolution: {integrity: sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google@3.0.31': + resolution: {integrity: sha512-RVNz8WFSIRbXbYDBE6JvlE2escWPJimBCs22LzKEYH7DNfl/X7cHNa1LFho4PsY6Ib0JmbzB8s2+i0wHs/wNCg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@3.0.36': + resolution: {integrity: sha512-foY3onGY8l3q9niMw0Cwe9xrYnm46keIWL57NRw6F3DKzSW9TYTfx0cQJs/j8lXJ8lPzqNxpMO/zXOkqCUt3IQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@3.0.41': + resolution: {integrity: sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.15': + resolution: {integrity: sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.19': + resolution: {integrity: sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + + '@ai-sdk/react@3.0.118': + resolution: {integrity: sha512-fBAix8Jftxse6/2YJnOFkwW1/O6EQK4DK68M9DlFmZGAzBmsaHXEPVS77sVIlkaOWCy11bE7434NAVXRY+3OsQ==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -727,10 +823,6 @@ packages: '@dimforge/rapier3d-compat@0.12.0': resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} - '@directus/sdk@21.1.0': - resolution: {integrity: sha512-Ig8zZAQDbc7QMIM54N+x71C04lni9MN9yalNAezjDjFdNknTJzupDY7V5cb+kOJL8GsqDE9Bg8xq8xCmkDVs5A==} - engines: {node: '>=22'} - '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -1420,6 +1512,12 @@ packages: '@hapi/topo@6.0.2': resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@html-validate/stylish@4.3.0': resolution: {integrity: sha512-eUfvKpRJg5TvzSfTf2EovrQoTKjkRnPUOUnXVJ2cQ4GbC/bQw98oxN+DdSf+HxOBK00YOhsP52xWdJPV1o4n5w==} engines: {node: '>= 18'} @@ -1577,6 +1675,9 @@ packages: cpu: [x64] os: [win32] + '@ioredis/commands@1.5.0': + resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1688,32 +1789,66 @@ packages: '@lhci/utils@0.15.1': resolution: {integrity: sha512-WclJnUQJeOMY271JSuaOjCv/aA0pgvuHZS29NFNdIeI14id8eiFsjith85EGKYhljgoQhJ2SiW4PsVfFiakNNw==} + '@mediapipe/tasks-vision@0.10.17': + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + '@medv/finder@4.0.2': resolution: {integrity: sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ==} - '@mintel/eslint-config@1.8.21': - resolution: {integrity: sha512-GH5tm1y89AhD+Lxf95BGCOdy7Nv1OPNLWrUpaTR6jsuKfH2dm9fU66LF7sDH5THmrkfAZ8zSzHJsKPjintv3IA==} + '@mintel/content-engine@1.9.10': + resolution: {integrity: sha512-rv5vJ1bQkW713q14tPLOgRt5Y7+t4tu76i3H3tJkCADAqGvLrM/QuI9SF6aR9H8/KHzzF7BjRHDFsHPYqzQhpg==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fcontent-engine/-/1.9.10/content-engine-1.9.10.tgz} - '@mintel/mail@1.8.21': - resolution: {integrity: sha512-leZV9gINmxD4eVJ3Ij9KdrQoyib67NVHgL/93J7KcWSUWKbr2HVuKUBpiWImeeEZn3JO0f7JwRbVUzXPBRVeQA==} + '@mintel/eslint-config@1.9.5': + resolution: {integrity: sha512-nZylW/99gnzkU/oCQNR5Muj6/gGYsf1EJSM8LSRAU3sU6wR3kUu4cba7LVQPb6uTrfW8CzD2JsL8pzanDqSZzA==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Feslint-config/-/1.9.5/eslint-config-1.9.5.tgz} + + '@mintel/journaling@1.9.10': + resolution: {integrity: sha512-wuOz6bhloVgw1yiA3OhRMxak7pqB5kft62QjxPz1w+LSRaRR4nDXRR1qhSdKlcqLhEJL5+kljJnVMZ3N2UhJnw==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fjournaling/-/1.9.10/journaling-1.9.10.tgz} + + '@mintel/mail@1.9.5': + resolution: {integrity: sha512-g7+pL2/NFrmjAgMUHHj4GTpWestabdIZBR0UkMiJBsEFpxu7TFJXorFw418w/z+G21JQDKLyQrT/NpNLaCFW+Q==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fmail/-/1.9.5/mail-1.9.5.tgz} peerDependencies: react: ^19.0.0 react-dom: ^19.0.0 - '@mintel/next-config@1.8.21': - resolution: {integrity: sha512-K4jb9Glf84a212BRZ/zmOUueBphmsikvStFCuDc5lxyFT+Hkj4w8ChmtI7gaUxHMrftooduGPXJ1+NFpKkvc/Q==} + '@mintel/meme-generator@1.9.10': + resolution: {integrity: sha512-Mg89RH4KgKOmpFF1G6DNu/LPzzQg2CgV15lazpsLa8ce/XydcSXOv2QvS2jk3kytGHOgqnu3j3xAn10Yi4KgvA==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fmeme-generator/-/1.9.10/meme-generator-1.9.10.tgz} - '@mintel/next-feedback@1.8.21': - resolution: {integrity: sha512-n2KzGDbOvAskuzjbt8h5EOMSEnISxHrsXxJwDdMxCXEgmzfJSvWpP2mAqb684dimOwo1UWHE6DMSAFc1FXeYwg==} + '@mintel/next-config@1.9.5': + resolution: {integrity: sha512-gnEtpoGXHjFwPIccU5GUoqqkAL1vni1uNtxSJ1XkG4z8HVQ9C3dH4IxxmUqMJaLL0fTSDOZpZKZIV0C1C28iyg==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fnext-config/-/1.9.5/next-config-1.9.5.tgz} + + '@mintel/next-feedback@1.9.5': + resolution: {integrity: sha512-7h9ClhTiEA86u+8hrdJohoPJUkVGy6txSo9P9/7//wS/CL81c5azV7GrGDS/pdGmAYdkVSp14i3/8Uq9lqTlZA==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fnext-feedback/-/1.9.5/next-feedback-1.9.5.tgz} peerDependencies: react: ^19.0.0 react-dom: ^19.0.0 - '@mintel/next-utils@1.8.21': - resolution: {integrity: sha512-sr0yDtySGou+3DNvrqY6HWSHCiVIc8nnoRbckyPMSE21AGxk2aJineXGy9BO9tulSBdhStm2SgdC7McMFTszug==} + '@mintel/next-utils@1.9.5': + resolution: {integrity: sha512-Ndcf2AONTccw8zMbsyFudubBsnoXfYuYoRPGq3d5OLcKfzlz+2g0ee8xcsNFwa9fpnAAMlvauYu0mIRGA/ydVA==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fnext-utils/-/1.9.5/next-utils-1.9.5.tgz} - '@mintel/tsconfig@1.8.21': - resolution: {integrity: sha512-ePBfBZiijyXKOS6nLIyxkg7QDZEEC1TugzNhmvwwpc0Yh7BmVHyNpvjg6zKsoGj2rok+9Kc8mLH1WihQIs8SKg==} + '@mintel/payload-ai@1.9.15': + resolution: {integrity: sha512-8TZ1imcLgOjXZsBt6fQMVLfkhsIRKz7tJqq5aTnOO28kwCb+TvTh+5AgZWKWxxTSXPYcpc3Zt97kNraOT18dzg==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fpayload-ai/-/1.9.15/payload-ai-1.9.15.tgz} + peerDependencies: + '@payloadcms/next': '>=3.0.0' + '@payloadcms/ui': '>=3.0.0' + payload: '>=3.0.0' + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@mintel/thumbnail-generator@1.9.10': + resolution: {integrity: sha512-3YcM4w7ysuffsxUJenx7RRSyTFhirDXDTgQ1KbRhNxkQH+9fCQUrH72w9lDNCZmP2776TSmwDPK46KD5CAGKzw==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fthumbnail-generator/-/1.9.10/thumbnail-generator-1.9.10.tgz} + + '@mintel/tsconfig@1.9.5': + resolution: {integrity: sha512-dxYJoGAE+9vwaPIpQL3NCzyEKMFYuJyDwWmCuUeP0lFo91brgSpHfl4cw98sK8UsrncIP6r4YvAgncooVnovaQ==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Ftsconfig/-/1.9.5/tsconfig-1.9.5.tgz} + + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true '@monaco-editor/loader@1.7.0': resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} @@ -1725,6 +1860,75 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@monogrid/gainmap-js@3.4.0': + resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==} + peerDependencies: + three: '>= 0.159.0' + + '@napi-rs/canvas-android-arm64@0.1.80': + resolution: {integrity: sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.80': + resolution: {integrity: sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.80': + resolution: {integrity: sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80': + resolution: {integrity: sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.80': + resolution: {integrity: sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-arm64-musl@0.1.80': + resolution: {integrity: sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.80': + resolution: {integrity: sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@napi-rs/canvas-linux-x64-gnu@0.1.80': + resolution: {integrity: sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-linux-x64-musl@0.1.80': + resolution: {integrity: sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-win32-x64-msvc@0.1.80': + resolution: {integrity: sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.80': + resolution: {integrity: sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==} + engines: {node: '>= 10'} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -2181,6 +2385,16 @@ packages: engines: {node: '>=18'} hasBin: true + '@qdrant/js-client-rest@1.17.0': + resolution: {integrity: sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A==} + engines: {node: '>=18.17.0', pnpm: '>=8'} + peerDependencies: + typescript: '>=4.7' + + '@qdrant/openapi-typescript-fetch@1.2.6': + resolution: {integrity: sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==} + engines: {node: '>=18.0.0', pnpm: '>=8'} + '@react-email/body@0.0.11': resolution: {integrity: sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==} peerDependencies: @@ -2522,6 +2736,42 @@ packages: '@react-pdf/types@2.9.2': resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==} + '@react-three/drei@10.7.7': + resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==} + peerDependencies: + '@react-three/fiber': ^9.0.0 + react: ^19 + react-dom: ^19 + three: '>=0.159' + peerDependenciesMeta: + react-dom: + optional: true + + '@react-three/fiber@9.5.0': + resolution: {integrity: sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-file-system: '>=11.0' + expo-gl: '>=11.0' + react: '>=19 <19.3' + react-dom: '>=19 <19.3' + react-native: '>=0.78' + three: '>=0.156' + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-file-system: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + '@reduxjs/toolkit@2.11.2': resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} peerDependencies: @@ -3119,6 +3369,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -3163,6 +3416,12 @@ packages: '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@22.19.10': resolution: {integrity: sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==} @@ -3175,6 +3434,9 @@ packages: '@types/nodemailer@7.0.9': resolution: {integrity: sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==} + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -3192,6 +3454,11 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react-transition-group@4.4.12': resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} peerDependencies: @@ -3302,6 +3569,9 @@ packages: resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -3397,6 +3667,18 @@ packages: cpu: [x64] os: [win32] + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + '@vitejs/plugin-react@5.1.4': resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3495,6 +3777,10 @@ packages: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + abs-svg-path@0.1.1: resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} @@ -3502,6 +3788,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -3545,6 +3835,22 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + ai@6.0.101: + resolution: {integrity: sha512-Ur/NgbgOp1rdhyDiKDk6EOpSgd1g5ADlbcD1cjQJtQsnmhEngz3Rf8nK5JetDh0vnbLy2aEBpaQeL+zvLRWuaA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + ai@6.0.116: + resolution: {integrity: sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -3746,6 +4052,9 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@4.0.3: resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} engines: {node: 20 || >=22} @@ -3833,6 +4142,10 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + body-scroll-lock@4.0.0-beta.0: resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==} @@ -3871,6 +4184,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -3899,6 +4215,12 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + camera-controls@3.1.2: + resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==} + engines: {node: '>=22.0.0', npm: '>=10.5.1'} + peerDependencies: + three: '>=0.126.1' + caniuse-lite@1.0.30001769: resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} @@ -4040,6 +4362,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -4063,6 +4389,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -4125,6 +4454,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -4151,6 +4484,10 @@ packages: cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -4194,6 +4531,11 @@ packages: resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==} engines: {node: '>=18.0'} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -4438,6 +4780,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -4450,6 +4796,9 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-gpu@5.0.70: + resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -4505,6 +4854,9 @@ packages: resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + drizzle-kit@0.31.7: resolution: {integrity: sha512-hOzRGSdyKIU4FcTSFYGKdXEjFsncVwHZ43gY3WU5Bz9j5Iadp6Rh6hxLSQ1IWXpKLBKt/d5y1cpSPcV+FcoQ1A==} hasBin: true @@ -4772,6 +5124,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} @@ -4929,6 +5285,10 @@ packages: event-stream@3.3.4: resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -4939,6 +5299,14 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -4947,10 +5315,20 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.3.0: + resolution: {integrity: sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.22.1: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -4958,6 +5336,9 @@ packages: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -5017,6 +5398,9 @@ packages: picomatch: optional: true + fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -5044,6 +5428,10 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} @@ -5085,10 +5473,17 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} @@ -5131,6 +5526,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + from@0.1.7: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} @@ -5257,6 +5656,9 @@ packages: resolution: {integrity: sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==} engines: {node: '>=0.10.0'} + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -5326,6 +5728,27 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} @@ -5335,9 +5758,16 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hls.js@1.6.15: + resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.12.5: + resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==} + engines: {node: '>=16.9.0'} + hoopy@0.1.4: resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==} engines: {node: '>= 6.0.0'} @@ -5362,6 +5792,9 @@ packages: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html-validate@10.8.0: resolution: {integrity: sha512-SJzCC9XzYOvyJWSHWYwXwqi8WIg6LcO3Pz+MGyxOSkMYiNerkUm/pv4uJsOM2pfHObX4r8EcPyhSjQ7jCdajnw==} engines: {node: ^20.19.0 || >= 22.12.0} @@ -5381,6 +5814,9 @@ packages: vitest: optional: true + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + html2canvas@1.4.1: resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} engines: {node: '>=8.0.0'} @@ -5422,6 +5858,9 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -5449,6 +5888,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + icu-minify@4.8.2: resolution: {integrity: sha512-LHBQV+skKkjZSPd590pZ7ZAHftUgda3eFjeuNwA8/15L8T8loCNBktKQyTlkodAU86KovFXeg/9WntlAo5wA5A==} @@ -5517,6 +5960,9 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + inquirer@6.5.2: resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} engines: {node: '>=6.0.0'} @@ -5535,6 +5981,10 @@ packages: intl-messageformat@11.1.2: resolution: {integrity: sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==} + ioredis@5.9.3: + resolution: {integrity: sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==} + engines: {node: '>=12.22.0'} + ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} @@ -5679,6 +6129,12 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -5764,6 +6220,11 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + its-fine@2.0.0: + resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} + peerDependencies: + react: ^19.0.0 + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -5793,6 +6254,9 @@ packages: jose@5.9.6: resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} + jose@6.2.0: + resolution: {integrity: sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -5862,6 +6326,9 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -6052,6 +6519,12 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -6119,6 +6592,12 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + maath@0.10.8: + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -6133,6 +6612,9 @@ packages: map-stream@0.1.0: resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@14.0.0: resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} engines: {node: '>= 18'} @@ -6163,15 +6645,45 @@ packages: md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + mdast-util-mdx-jsx@3.1.3: resolution: {integrity: sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==} + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} @@ -6188,6 +6700,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} @@ -6202,6 +6718,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -6209,6 +6729,11 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meshline@3.3.1: + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + meshoptimizer@1.0.1: resolution: {integrity: sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==} @@ -6222,6 +6747,27 @@ packages: micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-mdx-jsx@3.0.1: resolution: {integrity: sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg==} @@ -6475,6 +7021,11 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} @@ -6603,6 +7154,18 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openai@4.104.0: + resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -6743,6 +7306,9 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -6763,6 +7329,15 @@ packages: pdf-lib@1.17.1: resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==} + pdf-parse@2.4.5: + resolution: {integrity: sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==} + engines: {node: '>=20.16.0 <21 || >=22.3.0'} + hasBin: true + + pdfjs-dist@5.4.296: + resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==} + engines: {node: '>=20.16.0 || >=22.3.0'} + pdfkit@0.17.2: resolution: {integrity: sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==} @@ -6871,6 +7446,10 @@ packages: resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} hasBin: true + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} @@ -6937,6 +7516,9 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -6966,10 +7548,17 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + promise-worker-transferable@1.0.4: + resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -6977,6 +7566,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -7048,6 +7640,10 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-datepicker@7.6.0: resolution: {integrity: sha512-9cQH6Z/qa4LrGhzdc3XoHbhrxNcMi9MKjZmYgF/1MNNaJwvdSjv3Xd+jjvrEEbKEf71ZgCA3n7fQbdwd70qCRw==} peerDependencies: @@ -7106,6 +7702,12 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-promise-suspense@0.3.4: resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} @@ -7137,10 +7739,23 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -7161,6 +7776,14 @@ packages: react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + redux-thunk@3.1.0: resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} peerDependencies: @@ -7177,6 +7800,25 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + replicate@1.4.0: + resolution: {integrity: sha512-1ufKejfUVz/azy+5TnzQP7U1+MHVWZ6psnQ06az8byUUnRhT+DZ/MvewzB1NQYBVMgNKR7xPDtTwlcP5nv/5+w==} + engines: {git: '>=2.11.0', node: '>=18.0.0', npm: '>=7.19.0', yarn: '>=1.7.0'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -7257,6 +7899,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -7353,6 +7999,10 @@ packages: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -7360,6 +8010,10 @@ packages: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -7488,6 +8142,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + speedline-core@1.4.3: resolution: {integrity: sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==} engines: {node: '>=8.0'} @@ -7512,6 +8169,9 @@ packages: resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} engines: {node: '>=6'} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + start-server-and-test@2.1.3: resolution: {integrity: sha512-k4EcbNjeg0odaDkAMlIeDVDByqX9PIgL4tivgP2tES6Zd8o+4pTq/HgbWCyA3VHIoZopB+wGnNPKYGGSByNriQ==} engines: {node: '>=16'} @@ -7520,6 +8180,15 @@ packages: state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + stats-gl@2.4.2: + resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} + peerDependencies: + '@types/three': '*' + three: '*' + + stats.js@0.17.0: + resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -7644,6 +8313,12 @@ packages: stubborn-utils@1.0.2: resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -7676,12 +8351,22 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + svg-arc-to-cubic-bezier@3.2.0: resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==} svg-to-pdfkit@0.1.8: resolution: {integrity: sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==} + swr@2.4.1: + resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -7751,6 +8436,23 @@ packages: resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} engines: {node: '>=20'} + three-mesh-bvh@0.8.3: + resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} + peerDependencies: + three: '>= 0.159.0' + + three-stdlib@2.36.1: + resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==} + peerDependencies: + three: '>=0.128.0' + + three@0.183.1: + resolution: {integrity: sha512-Psv6bbd3d/M/01MT2zZ+VmD0Vj2dbWTNhfe4CuSg7w5TuW96M3NOyCVuh9SZQ05CpGmD7NEcJhZw4GVjhCYxfQ==} + + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -7836,6 +8538,25 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + troika-three-text@0.52.4: + resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.4: + resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} @@ -7874,6 +8595,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + turbo-darwin-64@2.8.10: resolution: {integrity: sha512-A03fXh+B7S8mL3PbdhTd+0UsaGrhfyPkODvzBDpKRY7bbeac4MDFpJ7I+Slf2oSkCEeSvHKR7Z4U71uKRUfX7g==} cpu: [x64] @@ -7924,6 +8648,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -7966,6 +8694,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -7987,6 +8718,9 @@ packages: unicode-trie@2.0.0: resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unique-string@2.0.0: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} @@ -7997,6 +8731,9 @@ packages: unist-util-position-from-estree@2.0.0: resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} @@ -8056,6 +8793,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -8083,9 +8824,15 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} @@ -8190,9 +8937,22 @@ packages: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + webdriver-bidi-protocol@0.4.1: resolution: {integrity: sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==} + webgl-constants@1.1.1: + resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} + + webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -8426,6 +9186,11 @@ packages: yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -8435,6 +9200,39 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + + zustand@5.0.11: + resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -8442,6 +9240,66 @@ snapshots: '@acemir/cssom@0.9.31': {} + '@ai-sdk/gateway@3.0.55(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.15(zod@3.25.76) + '@vercel/oidc': 3.1.0 + zod: 3.25.76 + + '@ai-sdk/gateway@3.0.66(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@3.25.76) + '@vercel/oidc': 3.1.0 + zod: 3.25.76 + + '@ai-sdk/google@3.0.31(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.15(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/openai@3.0.36(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.15(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/openai@3.0.41(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.15(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.19(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@3.0.118(react@19.2.4)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider-utils': 4.0.19(zod@3.25.76) + ai: 6.0.116(zod@3.25.76) + react: 19.2.4 + swr: 2.4.1(react@19.2.4) + throttleit: 2.1.0 + transitivePeerDependencies: + - zod + '@alloc/quick-lru@5.2.0': {} '@apidevtools/json-schema-ref-parser@11.9.3': @@ -8953,8 +9811,6 @@ snapshots: '@dimforge/rapier3d-compat@0.12.0': {} - '@directus/sdk@21.1.0': {} - '@discoveryjs/json-ext@0.5.7': {} '@dnd-kit/accessibility@3.1.1(react@19.2.4)': @@ -9485,6 +10341,10 @@ snapshots: dependencies: '@hapi/hoek': 11.0.7 + '@hono/node-server@1.19.11(hono@4.12.5)': + dependencies: + hono: 4.12.5 + '@html-validate/stylish@4.3.0': dependencies: kleur: 4.1.5 @@ -9596,6 +10456,8 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@ioredis/commands@1.5.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -9825,9 +10687,24 @@ snapshots: - supports-color - utf-8-validate + '@mediapipe/tasks-vision@0.10.17': {} + '@medv/finder@4.0.2': {} - '@mintel/eslint-config@1.8.21(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@mintel/content-engine@1.9.10(ws@8.19.0)(zod@3.25.76)': + dependencies: + '@mintel/journaling': 1.9.10(ws@8.19.0)(zod@3.25.76) + '@mintel/meme-generator': 1.9.10(ws@8.19.0)(zod@3.25.76) + '@mintel/thumbnail-generator': 1.9.10 + dotenv: 17.3.1 + openai: 4.104.0(ws@8.19.0)(zod@3.25.76) + transitivePeerDependencies: + - debug + - encoding + - ws + - zod + + '@mintel/eslint-config@1.9.5(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint/eslintrc': 3.3.4 '@eslint/js': 9.39.3 @@ -9844,13 +10721,31 @@ snapshots: - supports-color - typescript - '@mintel/mail@1.8.21(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@mintel/journaling@1.9.10(ws@8.19.0)(zod@3.25.76)': + dependencies: + axios: 1.13.5(debug@4.4.3) + openai: 4.104.0(ws@8.19.0)(zod@3.25.76) + transitivePeerDependencies: + - debug + - encoding + - ws + - zod + + '@mintel/mail@1.9.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@react-email/components': 0.0.33(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@mintel/next-config@1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0(esbuild@0.25.12))': + '@mintel/meme-generator@1.9.10(ws@8.19.0)(zod@3.25.76)': + dependencies: + openai: 4.104.0(ws@8.19.0)(zod@3.25.76) + transitivePeerDependencies: + - encoding + - ws + - zod + + '@mintel/next-config@1.9.5(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0(esbuild@0.25.12))': dependencies: '@sentry/nextjs': 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.0(esbuild@0.25.12)) next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) @@ -9873,9 +10768,8 @@ snapshots: - typescript - webpack - '@mintel/next-feedback@1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)': + '@mintel/next-feedback@1.9.5(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)': dependencies: - '@directus/sdk': 21.1.0 '@medv/finder': 4.0.2 clsx: 2.1.1 framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -9894,9 +10788,8 @@ snapshots: - babel-plugin-react-compiler - sass - '@mintel/next-utils@1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)': + '@mintel/next-utils@1.9.5(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)': dependencies: - '@directus/sdk': 21.1.0 next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) next-intl: 4.8.2(@swc/helpers@0.5.18)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(typescript@5.9.3) zod: 3.25.76 @@ -9912,7 +10805,57 @@ snapshots: - sass - typescript - '@mintel/tsconfig@1.8.21': {} + '@mintel/payload-ai@1.9.15(patch_hash=934315d3f15180552789a94bf76c34624501f601cc7409c24da3dc27af96c8fb)(@payloadcms/next@3.77.0(@types/react@19.2.13)(graphql@16.12.0)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(@payloadcms/ui@3.77.0(@types/react@19.2.13)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(ws@8.19.0)': + dependencies: + '@ai-sdk/openai': 3.0.41(zod@3.25.76) + '@ai-sdk/react': 3.0.118(react@19.2.4)(zod@3.25.76) + '@mintel/content-engine': 1.9.10(ws@8.19.0)(zod@3.25.76) + '@mintel/thumbnail-generator': 1.9.10 + '@modelcontextprotocol/sdk': 1.27.1(zod@3.25.76) + '@payloadcms/next': 3.77.0(@types/react@19.2.13)(graphql@16.12.0)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + '@payloadcms/ui': 3.77.0(@types/react@19.2.13)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + '@qdrant/js-client-rest': 1.17.0(typescript@5.9.3) + ai: 6.0.116(zod@3.25.76) + payload: 3.77.0(graphql@16.12.0)(typescript@5.9.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + replicate: 1.4.0 + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - debug + - encoding + - supports-color + - typescript + - ws + + '@mintel/thumbnail-generator@1.9.10': + dependencies: + replicate: 1.4.0 + + '@mintel/tsconfig@1.9.5': {} + + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.5) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.0(express@5.2.1) + hono: 4.12.5 + jose: 6.2.0 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color '@monaco-editor/loader@1.7.0': dependencies: @@ -9925,6 +10868,54 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + '@monogrid/gainmap-js@3.4.0(three@0.183.1)': + dependencies: + promise-worker-transferable: 1.0.4 + three: 0.183.1 + + '@napi-rs/canvas-android-arm64@0.1.80': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.80': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.80': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.80': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.80': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.80': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.80': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.80': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.80': + optional: true + + '@napi-rs/canvas@0.1.80': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.80 + '@napi-rs/canvas-darwin-arm64': 0.1.80 + '@napi-rs/canvas-darwin-x64': 0.1.80 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.80 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.80 + '@napi-rs/canvas-linux-arm64-musl': 0.1.80 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.80 + '@napi-rs/canvas-linux-x64-gnu': 0.1.80 + '@napi-rs/canvas-linux-x64-musl': 0.1.80 + '@napi-rs/canvas-win32-x64-msvc': 0.1.80 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.8.1 @@ -10572,6 +11563,14 @@ snapshots: - react-native-b4a - supports-color + '@qdrant/js-client-rest@1.17.0(typescript@5.9.3)': + dependencies: + '@qdrant/openapi-typescript-fetch': 1.2.6 + typescript: 5.9.3 + undici: 6.23.0 + + '@qdrant/openapi-typescript-fetch@1.2.6': {} + '@react-email/body@0.0.11(react@19.2.4)': dependencies: react: 19.2.4 @@ -10923,6 +11922,59 @@ snapshots: '@react-pdf/primitives': 4.1.1 '@react-pdf/stylesheet': 6.1.2 + '@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.13)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.1))(@types/react@19.2.13)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.4.0(three@0.183.1) + '@react-three/fiber': 9.5.0(@types/react@19.2.13)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.1) + '@use-gesture/react': 10.3.1(react@19.2.4) + camera-controls: 3.1.2(three@0.183.1) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.6.15 + maath: 0.10.8(@types/three@0.183.1)(three@0.183.1) + meshline: 3.3.1(three@0.183.1) + react: 19.2.4 + stats-gl: 2.4.2(@types/three@0.183.1)(three@0.183.1) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@19.2.4) + three: 0.183.1 + three-mesh-bvh: 0.8.3(three@0.183.1) + three-stdlib: 2.36.1(three@0.183.1) + troika-three-text: 0.52.4(three@0.183.1) + tunnel-rat: 0.1.2(@types/react@19.2.13)(immer@11.1.4)(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + utility-types: 3.11.0 + zustand: 5.0.11(@types/react@19.2.13)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + + '@react-three/fiber@9.5.0(@types/react@19.2.13)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@types/webxr': 0.5.24 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 2.0.0(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-use-measure: 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + scheduler: 0.27.0 + suspend-react: 0.1.3(react@19.2.4) + three: 0.183.1 + use-sync-external-store: 1.6.0(react@19.2.4) + zustand: 5.0.11(@types/react@19.2.13)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - immer + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.13)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': dependencies: '@standard-schema/spec': 1.1.0 @@ -11508,6 +12560,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/draco3d@1.4.10': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -11555,6 +12609,15 @@ snapshots: dependencies: '@types/node': 22.19.10 + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 22.19.13 + form-data: 4.0.5 + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@22.19.10': dependencies: undici-types: 6.21.0 @@ -11571,6 +12634,8 @@ snapshots: dependencies: '@types/node': 22.19.10 + '@types/offscreencanvas@2019.7.3': {} + '@types/parse-json@4.0.2': {} '@types/pg-pool@2.0.7': @@ -11593,6 +12658,10 @@ snapshots: dependencies: '@types/react': 19.2.13 + '@types/react-reconciler@0.28.9(@types/react@19.2.13)': + dependencies: + '@types/react': 19.2.13 + '@types/react-transition-group@4.4.12(@types/react@19.2.13)': dependencies: '@types/react': 19.2.13 @@ -11746,6 +12815,8 @@ snapshots: '@typescript-eslint/types': 8.56.1 eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.0': {} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -11805,6 +12876,15 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@19.2.4)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 19.2.4 + + '@vercel/oidc@3.1.0': {} + '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 @@ -11951,6 +13031,10 @@ snapshots: abbrev@2.0.0: {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + abs-svg-path@0.1.1: {} accepts@1.3.8: @@ -11958,6 +13042,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -11992,6 +13081,26 @@ snapshots: agent-base@7.1.4: {} + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + ai@6.0.101(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 3.0.55(zod@3.25.76) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.15(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + + ai@6.0.116(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 3.0.66(zod@3.25.76) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -12000,6 +13109,10 @@ snapshots: optionalDependencies: ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv-keywords@5.1.0(ajv@8.18.0): dependencies: ajv: 8.18.0 @@ -12207,6 +13320,8 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.11 + bail@2.0.2: {} + balanced-match@4.0.3: {} balanced-match@4.0.4: {} @@ -12291,6 +13406,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + body-scroll-lock@4.0.0-beta.0: {} boolbase@1.0.0: {} @@ -12329,6 +13458,11 @@ snapshots: buffer-from@1.1.2: {} + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -12356,6 +13490,10 @@ snapshots: camelcase@5.3.1: {} + camera-controls@3.1.2(three@0.183.1): + dependencies: + three: 0.183.1 + caniuse-lite@1.0.30001769: {} ccount@2.0.1: {} @@ -12526,6 +13664,8 @@ snapshots: clsx@2.1.1: {} + cluster-key-slot@1.1.2: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -12549,6 +13689,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} commander@13.1.0: {} @@ -12626,6 +13768,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} conventional-changelog-angular@8.1.0: @@ -12646,6 +13790,8 @@ snapshots: cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} + cookie@0.7.2: {} core-js@3.48.0: {} @@ -12693,6 +13839,10 @@ snapshots: croner@9.1.0: {} + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -12961,12 +14111,18 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} dequal@2.0.3: {} destroy@1.2.0: {} + detect-gpu@5.0.70: + dependencies: + webgl-constants: 1.1.1 + detect-libc@2.1.2: {} devlop@1.1.0: @@ -13022,6 +14178,8 @@ snapshots: dotenv@17.3.1: {} + draco3d@1.5.7: {} + drizzle-kit@0.31.7: dependencies: '@drizzle-team/brocli': 0.10.2 @@ -13332,6 +14490,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + escodegen@2.1.0: dependencies: esprima: 4.0.1 @@ -13579,6 +14739,8 @@ snapshots: stream-combiner: 0.0.4 through: 2.3.8 + event-target-shim@5.0.1: {} + eventemitter3@5.0.4: {} events-universal@1.0.1: @@ -13589,6 +14751,12 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -13603,6 +14771,11 @@ snapshots: expect-type@1.3.0: {} + express-rate-limit@8.3.0(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + express@4.22.1: dependencies: accepts: 1.3.8 @@ -13639,12 +14812,47 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.8: {} extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 + extend@3.0.2: {} + external-editor@3.1.0: dependencies: chardet: 0.7.0 @@ -13701,6 +14909,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.6.10: {} + fflate@0.8.2: {} figures@2.0.0: @@ -13735,6 +14945,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-root@1.1.0: {} find-up@4.1.0: @@ -13783,6 +15004,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data-encoder@1.7.2: {} + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -13791,6 +15014,11 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + forwarded-parse@2.1.2: {} forwarded@0.2.0: {} @@ -13817,6 +15045,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + from@0.1.7: {} fs.realpath@1.0.0: {} @@ -13963,6 +15193,8 @@ snapshots: pify: 2.3.0 pinkie-promise: 2.0.1 + glsl-noise@0.0.0: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -14029,6 +15261,79 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + help-me@5.0.0: {} hermes-estree@0.25.1: {} @@ -14037,10 +15342,14 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hls.js@1.6.15: {} + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 + hono@4.12.5: {} + hoopy@0.1.4: {} hsl-to-hex@1.0.0: @@ -14069,6 +15378,8 @@ snapshots: htmlparser2: 8.0.2 selderee: 0.11.0 + html-url-attributes@3.0.1: {} + html-validate@10.8.0(vitest@4.0.18): dependencies: '@html-validate/stylish': 4.3.0 @@ -14082,6 +15393,8 @@ snapshots: optionalDependencies: vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + html-void-elements@3.0.0: {} + html2canvas@1.4.1: dependencies: css-line-break: 2.1.0 @@ -14143,6 +15456,10 @@ snapshots: human-signals@2.1.0: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + husky@9.1.7: {} hyphen@1.14.1: {} @@ -14163,6 +15480,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + icu-minify@4.8.2: dependencies: '@formatjs/icu-messageformat-parser': 3.5.1 @@ -14221,6 +15542,8 @@ snapshots: ini@4.1.1: {} + inline-style-parser@0.2.7: {} + inquirer@6.5.2: dependencies: ansi-escapes: 3.2.0 @@ -14259,6 +15582,20 @@ snapshots: '@formatjs/icu-messageformat-parser': 3.5.1 tslib: 2.8.1 + ioredis@5.9.3: + dependencies: + '@ioredis/commands': 1.5.0 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-address@10.1.0: {} ipaddr.js@1.9.1: {} @@ -14381,6 +15718,10 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@2.2.2: {} + + is-promise@4.0.0: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -14464,6 +15805,13 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + its-fine@2.0.0(@types/react@19.2.13)(react@19.2.4): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.2.13) + react: 19.2.4 + transitivePeerDependencies: + - '@types/react' + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -14500,6 +15848,8 @@ snapshots: jose@5.9.6: {} + jose@6.2.0: {} + joycon@3.1.1: {} jpeg-exif@1.1.4: {} @@ -14581,6 +15931,8 @@ snapshots: json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -14784,6 +16136,10 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + lodash.kebabcase@4.1.1: {} lodash.merge@4.6.2: {} @@ -14842,6 +16198,11 @@ snapshots: dependencies: react: 19.2.4 + maath@0.10.8(@types/three@0.183.1)(three@0.183.1): + dependencies: + '@types/three': 0.183.1 + three: 0.183.1 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -14856,6 +16217,8 @@ snapshots: map-stream@0.1.0: {} + markdown-table@3.0.4: {} + marked@14.0.0: {} marked@15.0.12: {} @@ -14877,6 +16240,13 @@ snapshots: crypt: 0.0.2 is-buffer: 1.1.6 + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + mdast-util-from-markdown@2.0.2: dependencies: '@types/mdast': 4.0.4 @@ -14894,6 +16264,74 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-jsx@3.1.3: dependencies: '@types/estree-jsx': 1.0.5 @@ -14911,11 +16349,34 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 unist-util-is: 6.0.1 + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + mdast-util-to-markdown@2.1.2: dependencies: '@types/mdast': 4.0.4 @@ -14938,6 +16399,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + memoize-one@6.0.0: {} meow@12.1.1: {} @@ -14946,10 +16409,16 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} + meshline@3.3.1(three@0.183.1): + dependencies: + three: 0.183.1 + meshoptimizer@1.0.1: {} metaviewport-parser@0.3.0: {} @@ -14975,6 +16444,64 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + micromark-extension-mdx-jsx@3.0.1: dependencies: '@types/acorn': 4.0.6 @@ -15280,6 +16807,8 @@ snapshots: node-addon-api@7.1.1: {} + node-domexception@1.0.0: {} + node-exports-info@1.6.0: dependencies: array.prototype.flatmap: 1.3.3 @@ -15411,6 +16940,21 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openai@4.104.0(ws@8.19.0)(zod@3.25.76): + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + ws: 8.19.0 + zod: 3.25.76 + transitivePeerDependencies: + - encoding + opener@1.5.2: {} optionator@0.9.4: @@ -15608,6 +17152,8 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -15662,6 +17208,15 @@ snapshots: pako: 1.0.11 tslib: 1.14.1 + pdf-parse@2.4.5: + dependencies: + '@napi-rs/canvas': 0.1.80 + pdfjs-dist: 5.4.296 + + pdfjs-dist@5.4.296: + optionalDependencies: + '@napi-rs/canvas': 0.1.80 + pdfkit@0.17.2: dependencies: crypto-js: 4.2.0 @@ -15809,6 +17364,8 @@ snapshots: sonic-boom: 4.2.0 thread-stream: 3.1.0 + pkce-challenge@5.0.1: {} + pkg-types@2.3.0: dependencies: confbox: 0.2.4 @@ -15861,6 +17418,8 @@ snapshots: postgres-range@1.1.4: {} + potpack@1.0.2: {} + prelude-ls@1.2.1: {} prepend-http@3.0.1: {} @@ -15875,8 +17434,16 @@ snapshots: process-warning@5.0.0: {} + process@0.11.10: + optional: true + progress@2.0.3: {} + promise-worker-transferable@1.0.4: + dependencies: + is-promise: 2.2.2 + lie: 3.1.1 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -15888,6 +17455,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.1.0: {} + proto-list@1.2.4: {} protocolify@3.0.0: @@ -16004,6 +17573,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-datepicker@7.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@floating-ui/react': 0.27.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -16075,6 +17651,24 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + react-markdown@10.1.0(@types/react@19.2.13)(react@19.2.4): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.13 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.4 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-promise-suspense@0.3.4: dependencies: fast-deep-equal: 2.0.1 @@ -16116,8 +17710,23 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + react-use-measure@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + react@19.2.4: {} + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + optional: true + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -16146,6 +17755,12 @@ snapshots: - '@types/react' - redux + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + redux-thunk@3.1.0(redux@5.0.1): dependencies: redux: 5.0.1 @@ -16172,6 +17787,50 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + replicate@1.4.0: + optionalDependencies: + readable-stream: 4.7.0 + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -16272,6 +17931,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-async@2.4.1: {} run-parallel@1.2.0: @@ -16383,6 +18052,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -16396,6 +18081,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: {} set-function-length@1.2.2: @@ -16585,6 +18279,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + speedline-core@1.4.3: dependencies: '@types/node': 22.19.10 @@ -16607,6 +18303,8 @@ snapshots: dependencies: type-fest: 0.7.1 + standard-as-callback@2.1.0: {} + start-server-and-test@2.1.3: dependencies: arg: 5.0.2 @@ -16622,6 +18320,13 @@ snapshots: state-local@1.0.7: {} + stats-gl@2.4.2(@types/three@0.183.1)(three@0.183.1): + dependencies: + '@types/three': 0.183.1 + three: 0.183.1 + + stats.js@0.17.0: {} + statuses@2.0.2: {} std-env@3.10.0: {} @@ -16774,6 +18479,14 @@ snapshots: stubborn-utils@1.0.2: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): dependencies: client-only: 0.0.1 @@ -16797,12 +18510,22 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + suspend-react@0.1.3(react@19.2.4): + dependencies: + react: 19.2.4 + svg-arc-to-cubic-bezier@3.2.0: {} svg-to-pdfkit@0.1.8: dependencies: pdfkit: 0.17.2 + swr@2.4.1(react@19.2.4): + dependencies: + dequal: 2.0.3 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + symbol-tree@3.2.4: {} tabbable@6.4.0: {} @@ -16878,6 +18601,24 @@ snapshots: dependencies: real-require: 0.2.0 + three-mesh-bvh@0.8.3(three@0.183.1): + dependencies: + three: 0.183.1 + + three-stdlib@2.36.1(three@0.183.1): + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.24 + draco3d: 1.5.7 + fflate: 0.6.10 + potpack: 1.0.2 + three: 0.183.1 + + three@0.183.1: {} + + throttleit@2.1.0: {} + through@2.3.8: {} tiny-inflate@1.0.3: {} @@ -16951,6 +18692,24 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + + troika-three-text@0.52.4(three@0.183.1): + dependencies: + bidi-js: 1.0.3 + three: 0.183.1 + troika-three-utils: 0.52.4(three@0.183.1) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.4(three@0.183.1): + dependencies: + three: 0.183.1 + + troika-worker-utils@0.52.0: {} + + trough@2.2.0: {} + truncate-utf8-bytes@1.0.2: dependencies: utf8-byte-length: 1.0.5 @@ -16989,6 +18748,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-rat@0.1.2(@types/react@19.2.13)(immer@11.1.4)(react@19.2.4): + dependencies: + zustand: 4.5.7(@types/react@19.2.13)(immer@11.1.4)(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - immer + - react + turbo-darwin-64@2.8.10: optional: true @@ -17031,6 +18798,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -17092,6 +18865,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@5.26.5: {} + undici-types@6.21.0: {} undici@6.23.0: {} @@ -17110,6 +18885,16 @@ snapshots: pako: 0.2.9 tiny-inflate: 1.0.3 + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + unique-string@2.0.0: dependencies: crypto-random-string: 2.0.0 @@ -17122,6 +18907,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 @@ -17207,6 +18996,8 @@ snapshots: util-deprecate@1.0.2: {} + utility-types@3.11.0: {} + utils-merge@1.0.1: {} utrie@1.0.2: @@ -17223,11 +19014,21 @@ snapshots: vary@1.1.2: {} + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + victory-vendor@37.3.6: dependencies: '@types/d3-array': 3.2.2 @@ -17335,8 +19136,16 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + web-namespaces@2.0.1: {} + + web-streams-polyfill@4.0.0-beta.3: {} + webdriver-bidi-protocol@0.4.1: {} + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + webidl-conversions@3.0.1: {} webidl-conversions@8.0.1: {} @@ -17591,10 +19400,29 @@ snapshots: yoga-layout@3.2.1: {} + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-validation-error@4.0.2(zod@3.25.76): dependencies: zod: 3.25.76 zod@3.25.76: {} + zustand@4.5.7(@types/react@19.2.13)(immer@11.1.4)(react@19.2.4): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + immer: 11.1.4 + react: 19.2.4 + + zustand@5.0.11(@types/react@19.2.13)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + optionalDependencies: + '@types/react': 19.2.13 + immer: 11.1.4 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + zwitch@2.0.4: {} diff --git a/scripts/qdrant-sync.sh b/scripts/qdrant-sync.sh new file mode 100755 index 00000000..21f37840 --- /dev/null +++ b/scripts/qdrant-sync.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# ──────────────────────────────────────────────────────────────────────────── +# Qdrant Snapshot Sync Tool +# Syncs a Qdrant collection from the local machine to a remote environment +# using the safe Snapshot API to avoid RocksDB corruption. +# ──────────────────────────────────────────────────────────────────────────── +set -euo pipefail + +# Load environment variables +if [ -f .env ]; then + set -a; source .env; set +a +fi + +# ── Configuration ────────────────────────────────────────────────────────── +TARGET_ENV="${1:-}" # testing | staging | branch_slug | prod +COLLECTION="${2:-kabelfachmann}" +SSH_HOST="root@alpha.mintel.me" + +if [[ -z "$TARGET_ENV" ]]; then + echo "Usage: pnpm run qdrant:push [collection]" + echo "Example: pnpm run qdrant:push testing kabelfachmann" + echo "Example: pnpm run qdrant:push mein-feature-slug kabelfachmann" + exit 1 +fi + +LOCAL_QDRANT_URL=${QDRANT_URL:-"http://localhost:6337"} +TIMEOUT=300 # 5 minutes for large snapshots + +get_target_path() { + case "$1" in + testing) echo "/home/deploy/sites/testing.klz-cables.com" ;; + staging) echo "/home/deploy/sites/staging.klz-cables.com" ;; + prod|production) echo "/home/deploy/sites/klz-cables.com" ;; + *) echo "/home/deploy/sites/branch.klz-cables.com/$1" ;; + esac +} + +get_project_name() { + case "$1" in + testing) echo "klz-testing" ;; + staging) echo "klz-staging" ;; + prod|production) echo "klz-cablescom" ;; + *) echo "klz-branch-$1" ;; + esac +} + +TGT_PATH=$(get_target_path "$TARGET_ENV") +PROJECT_NAME=$(get_project_name "$TARGET_ENV") +QDRANT_CONTAINER="${PROJECT_NAME}-klz-qdrant-1" +WORK_DIR=$(mktemp -d) + +echo "🚀 Syncing Qdrant Collection '$COLLECTION' to: $TARGET_ENV" + +# 1. Create Snapshot Locally +echo "📸 1/5 Creating snapshot on local Qdrant ($LOCAL_QDRANT_URL)..." +SNAPSHOT_INFO=$(curl --max-time $TIMEOUT -s -X POST "$LOCAL_QDRANT_URL/collections/$COLLECTION/snapshots") + +if ! echo "$SNAPSHOT_INFO" | grep -q '"status":"ok"'; then + echo "❌ Failed to create snapshot." + echo "Response: $SNAPSHOT_INFO" + exit 1 +fi + +SNAPSHOT_NAME=$(echo "$SNAPSHOT_INFO" | grep -o '"name":"[^"]*' | cut -d'"' -f4) +echo " ✅ Snapshot created: $SNAPSHOT_NAME" + +# 2. Download Snapshot +echo "⬇️ 2/5 Downloading snapshot..." +curl --max-time $TIMEOUT -s -o "$WORK_DIR/$SNAPSHOT_NAME" "$LOCAL_QDRANT_URL/collections/$COLLECTION/snapshots/$SNAPSHOT_NAME" +echo " ✅ Downloaded to $WORK_DIR/$SNAPSHOT_NAME" + +# 3. Compress and Transfer Snapshot +echo "📦 3/6 Compressing snapshot to save bandwidth..." +gzip -c "$WORK_DIR/$SNAPSHOT_NAME" > "$WORK_DIR/$SNAPSHOT_NAME.gz" +echo " ✅ Compressed $SNAPSHOT_NAME.gz" + +echo "📤 4/6 Uploading compressed snapshot to Alpha ($SSH_HOST)..." +SSH_OPTS="-o ServerAliveInterval=60 -o ServerAliveCountMax=10 -o ConnectTimeout=30" +ssh $SSH_OPTS "$SSH_HOST" "mkdir -p $TGT_PATH/qdrant_tmp" +rsync --partial --progress --timeout=600 -e "ssh $SSH_OPTS" \ + "$WORK_DIR/$SNAPSHOT_NAME.gz" "$SSH_HOST:$TGT_PATH/qdrant_tmp/$SNAPSHOT_NAME.gz" +echo " ✅ Upload complete." + +# 4. Restore Snapshot on Remote Server +echo "🔄 5/6 Restoring snapshot on target container ($QDRANT_CONTAINER)..." + +# Qdrant restore process: +# - Extract snapshot on server +# - Recreate collection (so it is clean) +# - Download snapshot to container +# - Recover from snapshot file + +ssh $SSH_OPTS "$SSH_HOST" << EOF + set -e + # Step A: Extract the compressed file + echo " [Remote] Extracting snapshot..." + gunzip -f "$TGT_PATH/qdrant_tmp/$SNAPSHOT_NAME.gz" + + # Step B: Copy file into the container + docker cp "$TGT_PATH/qdrant_tmp/$SNAPSHOT_NAME" $QDRANT_CONTAINER:/qdrant/$SNAPSHOT_NAME + + # Step C: Delete existing collection + curl -s -X DELETE "http://127.0.0.1:6333/collections/$COLLECTION" > /dev/null + + # Step D: Re-create empty collection (required before recovery) + # wir nutzen die standard vector config vom Kabelfachmann (Cosine, 384 dim für all-MiniLM-L6-v2) + curl -s -X PUT "http://127.0.0.1:6333/collections/$COLLECTION" \ + -H 'Content-Type: application/json' \ + -d '{ "vectors": { "size": 384, "distance": "Cosine" } }' > /dev/null + + # Step E: Recover + echo " [Remote] Triggering recover API..." + curl -s -X PUT "http://127.0.0.1:6333/collections/$COLLECTION/snapshots/recover" \ + -H 'Content-Type: application/json' \ + -d '{ "location": "file:///qdrant/'$SNAPSHOT_NAME'" }' > /dev/null + + # Step F: Cleanup + docker exec $QDRANT_CONTAINER rm /qdrant/$SNAPSHOT_NAME + rm -rf "$TGT_PATH/qdrant_tmp" +EOF + +echo " ✅ Restore complete." + +# 5. Local Cleanup +echo "🧹 6/6 Cleaning up..." +rm -rf "$WORK_DIR" +# Delete snapshot from local Qdrant server to save space +curl -s -X DELETE "$LOCAL_QDRANT_URL/collections/$COLLECTION/snapshots/$SNAPSHOT_NAME" > /dev/null +echo " ✅ Local cleanup done." + +echo "" +echo "🎉 Successfully synced Qdrant collection '$COLLECTION' to $TARGET_ENV!" diff --git a/src/lib/qdrant.ts b/src/lib/qdrant.ts new file mode 100644 index 00000000..c477e920 --- /dev/null +++ b/src/lib/qdrant.ts @@ -0,0 +1,195 @@ +import { QdrantClient } from '@qdrant/js-client-rest'; +import redis from './redis'; + +const isDockerContainer = + process.env.IS_DOCKER === 'true' || process.env.HOSTNAME?.includes('klz-app'); +const qdrantUrl = + process.env.QDRANT_URL || + (isDockerContainer ? 'http://klz-qdrant:6333' : 'http://localhost:6333'); +const qdrantApiKey = process.env.QDRANT_API_KEY || ''; + +export const qdrant = new QdrantClient({ + url: qdrantUrl, + apiKey: qdrantApiKey || undefined, + // Disable qdrant client's own version check to avoid the warning spam + checkCompatibility: false, +}); + +export const COLLECTION_NAME = 'klz_products'; +export const VECTOR_SIZE = 1024; // Mistral mistral-embed + +// Cache TTLs +const EMBEDDING_CACHE_TTL = 60 * 60 * 24; // 24h — embeddings are deterministic +const SEARCH_CACHE_TTL = 60 * 30; // 30 min — product data could change + +// Track collection existence in-memory (don't re-check every request) +let collectionVerified = false; + +/** + * Ensure the collection exists in Qdrant (only checks once per process lifetime). + */ +export async function ensureCollection() { + if (collectionVerified) return; + + try { + const collections = await qdrant.getCollections(); + const exists = collections.collections.some((c) => c.name === COLLECTION_NAME); + if (!exists) { + await qdrant.createCollection(COLLECTION_NAME, { + vectors: { + size: VECTOR_SIZE, + distance: 'Cosine', + }, + }); + console.log(`Successfully created Qdrant collection: ${COLLECTION_NAME}`); + } + collectionVerified = true; + } catch (error) { + console.error('Error ensuring Qdrant collection:', error); + } +} + +/** + * Hash text for cache key + */ +function hashKey(text: string): string { + const { createHash } = require('crypto'); + return createHash('sha256').update(text).digest('hex').slice(0, 32); +} + +/** + * Generate embedding using Mistral API (EU/DSGVO-compliant) + */ +export async function generateEmbedding(text: string): Promise { + const cacheKey = `emb:${hashKey(text.toLowerCase().trim())}`; + + // Try Redis cache first + try { + const cached = await redis.get(cacheKey); + if (cached) { + return JSON.parse(cached); + } + } catch { + // Redis down — proceed without cache + } + + const mistralKey = process.env.MISTRAL_API_KEY; + if (!mistralKey) { + throw new Error('MISTRAL_API_KEY is not set'); + } + + const response = await fetch('https://api.mistral.ai/v1/embeddings', { + method: 'POST', + headers: { + Authorization: `Bearer ${mistralKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'mistral-embed', + input: [text], + }), + }); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error( + `Failed to generate embedding: ${response.status} ${response.statusText} ${errorBody}`, + ); + } + + const data = await response.json(); + const embedding = data.data[0].embedding; + + // Cache the embedding in Redis + try { + await redis.set(cacheKey, JSON.stringify(embedding), 'EX', EMBEDDING_CACHE_TTL); + } catch { + // Redis down — proceed without caching + } + + return embedding; +} + +/** + * Upsert a product into Qdrant + */ +export async function upsertProductVector( + id: string | number, + text: string, + payload: Record, +) { + try { + await ensureCollection(); + const vector = await generateEmbedding(text); + + await qdrant.upsert(COLLECTION_NAME, { + wait: true, + points: [ + { + id: id, + vector, + payload, + }, + ], + }); + } catch (error) { + console.error('Error writing to Qdrant:', error); + } +} + +/** + * Delete a product from Qdrant + */ +export async function deleteProductVector(id: string | number) { + try { + await ensureCollection(); + await qdrant.delete(COLLECTION_NAME, { + wait: true, + points: [id] as [string | number], + }); + } catch (error) { + console.error('Error deleting from Qdrant:', error); + } +} + +/** + * Search products in Qdrant. + * Results are cached in Redis for 30 minutes keyed by query text. + */ +export async function searchProducts(query: string, limit = 5) { + const cacheKey = `search:${hashKey(query.toLowerCase().trim())}:${limit}`; + + // Try Redis cache first + try { + const cached = await redis.get(cacheKey); + if (cached) { + console.log(`[Qdrant] Cache HIT for query: "${query.substring(0, 50)}"`); + return JSON.parse(cached); + } + } catch { + // Redis down — proceed without cache + } + + try { + await ensureCollection(); + const vector = await generateEmbedding(query); + + const results = await qdrant.search(COLLECTION_NAME, { + vector, + limit, + with_payload: true, + }); + + // Cache results in Redis + try { + await redis.set(cacheKey, JSON.stringify(results), 'EX', SEARCH_CACHE_TTL); + } catch { + // Redis down — proceed without caching + } + + return results; + } catch (error) { + console.error('Error searching in Qdrant:', error); + return []; + } +} diff --git a/src/lib/redis.ts b/src/lib/redis.ts new file mode 100644 index 00000000..93b6cd0d --- /dev/null +++ b/src/lib/redis.ts @@ -0,0 +1,22 @@ +import Redis from 'ioredis'; + +const isDockerContainer = + process.env.IS_DOCKER === 'true' || process.env.HOSTNAME?.includes('klz-app'); +const redisUrl = + process.env.REDIS_URL || + (isDockerContainer ? 'redis://klz-redis:6379' : 'redis://localhost:6379'); + +// Only create a single instance in Node.js +const globalForRedis = global as unknown as { redis: Redis }; + +export const redis = + globalForRedis.redis || + new Redis(redisUrl, { + maxRetriesPerRequest: 3, + }); + +if (process.env.NODE_ENV !== 'production') { + globalForRedis.redis = redis; +} + +export default redis; diff --git a/src/migrations/20260225_175000_native_localization.ts b/src/migrations/20260225_175000_native_localization.ts index 20d81a09..a5ba9dd5 100644 --- a/src/migrations/20260225_175000_native_localization.ts +++ b/src/migrations/20260225_175000_native_localization.ts @@ -55,10 +55,29 @@ export async function up({ db }: MigrateUpArgs): Promise { CREATE TYPE "public"."enum__products_v_version_status" AS ENUM('draft', 'published'); EXCEPTION WHEN duplicate_object THEN null; END $$ `); + await db.execute(sql` + DO $$ BEGIN + CREATE TYPE "public"."enum_pages_status" AS ENUM('draft', 'published'); + EXCEPTION WHEN duplicate_object THEN null; END $$ + `); + await db.execute(sql` + DO $$ BEGIN + CREATE TYPE "public"."enum_posts_status" AS ENUM('draft', 'published'); + EXCEPTION WHEN duplicate_object THEN null; END $$ + `); + await db.execute(sql` + DO $$ BEGIN + CREATE TYPE "public"."enum_products_status" AS ENUM('draft', 'published'); + EXCEPTION WHEN duplicate_object THEN null; END $$ + `); // ── 2. Alter pages table ───────────────────────────────────────────────────── - await db.execute(sql`ALTER TABLE "pages" ADD COLUMN IF NOT EXISTS "layout" "enum_pages_layout" DEFAULT 'default'`); - await db.execute(sql`ALTER TABLE "pages" ADD COLUMN IF NOT EXISTS "_status" "enum_pages_status" DEFAULT 'draft'`); + await db.execute( + sql`ALTER TABLE "pages" ADD COLUMN IF NOT EXISTS "layout" "enum_pages_layout" DEFAULT 'default'`, + ); + await db.execute( + sql`ALTER TABLE "pages" ADD COLUMN IF NOT EXISTS "_status" "enum_pages_status" DEFAULT 'draft'`, + ); // ── 3. Create pages_locales join table ─────────────────────────────────────── await db.execute(sql` @@ -202,9 +221,63 @@ export async function up({ db }: MigrateUpArgs): Promise { await db.execute(sql`ALTER TABLE "products" DROP COLUMN IF EXISTS "locale"`); // ── 12. Version tables (_posts_v, _products_v, _pages_v) locale columns ────── - await db.execute(sql`ALTER TABLE "_posts_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__posts_v_published_locale"`); - await db.execute(sql`ALTER TABLE "_products_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__products_v_published_locale"`); - await db.execute(sql`ALTER TABLE "_pages_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__pages_v_published_locale"`); + await db.execute(sql` + CREATE TABLE IF NOT EXISTS "_posts_v" ( + "id" serial PRIMARY KEY NOT NULL, + "parent_id" integer, + "version_title" varchar, + "version_slug" varchar, + "version_excerpt" varchar, + "version_content" jsonb, + "version_updated_at" timestamp(3) with time zone, + "version_created_at" timestamp(3) with time zone, + "version__status" "enum__posts_v_version_status" DEFAULT 'draft', + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "latest" boolean + ) + `); + await db.execute(sql` + CREATE TABLE IF NOT EXISTS "_products_v" ( + "id" serial PRIMARY KEY NOT NULL, + "parent_id" integer, + "version_title" varchar, + "version_description" varchar, + "version_content" jsonb, + "version_updated_at" timestamp(3) with time zone, + "version_created_at" timestamp(3) with time zone, + "version__status" "enum__products_v_version_status" DEFAULT 'draft', + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "latest" boolean + ) + `); + await db.execute(sql` + CREATE TABLE IF NOT EXISTS "_pages_v" ( + "id" serial PRIMARY KEY NOT NULL, + "parent_id" integer, + "version_title" varchar, + "version_slug" varchar, + "version_excerpt" varchar, + "version_content" jsonb, + "version_updated_at" timestamp(3) with time zone, + "version_created_at" timestamp(3) with time zone, + "version__status" "enum__pages_v_version_status" DEFAULT 'draft', + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "latest" boolean + ) + `); + + await db.execute( + sql`ALTER TABLE "_posts_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__posts_v_published_locale"`, + ); + await db.execute( + sql`ALTER TABLE "_products_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__products_v_published_locale"`, + ); + await db.execute( + sql`ALTER TABLE "_pages_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__pages_v_published_locale"`, + ); // ── 13. Create _posts_v_locales ────────────────────────────────────────────── await db.execute(sql` diff --git a/src/payload/collections/Pages.ts b/src/payload/collections/Pages.ts index 517ce61f..03de368f 100644 --- a/src/payload/collections/Pages.ts +++ b/src/payload/collections/Pages.ts @@ -26,6 +26,66 @@ export const Pages: CollectionConfig = { }; }, }, + hooks: { + afterChange: [ + async ({ doc, req }) => { + // Run index sync asynchronously to not block the CMS save operation + setTimeout(async () => { + try { + const { upsertProductVector, deleteProductVector } = await import('../../lib/qdrant'); + + // Check if page is published + if (doc._status !== 'published') { + await deleteProductVector(`page_${doc.id}`); + req.payload.logger.info(`Removed drafted page ${doc.slug} from Qdrant`); + } else { + // Serialize payload + const contentText = [ + `Seite: ${doc.title}`, + doc.excerpt ? `Beschreibung: ${doc.excerpt}` : '', + ] + .filter(Boolean) + .join('\n'); + + const payload = { + type: 'knowledge', + content: contentText, + data: { + title: doc.title, + slug: doc.slug, + }, + }; + + await upsertProductVector(`page_${doc.id}`, contentText, payload); + req.payload.logger.info(`Upserted page ${doc.slug} to Qdrant`); + } + } catch (error) { + req.payload.logger.error({ + msg: 'Error syncing page to Qdrant', + err: error, + pageId: doc.id, + }); + } + }, 0); + return doc; + }, + ], + afterDelete: [ + async ({ id, req }) => { + try { + const { deleteProductVector } = await import('../../lib/qdrant'); + await deleteProductVector(`page_${id}`); + req.payload.logger.info(`Deleted page ${id} from Qdrant`); + } catch (error) { + req.payload.logger.error({ + msg: 'Error deleting page from Qdrant', + err: error, + pageId: id, + }); + } + }, + ], + }, fields: [ { name: 'title', diff --git a/src/payload/collections/Posts.ts b/src/payload/collections/Posts.ts index a497b8de..8cbb4c9b 100644 --- a/src/payload/collections/Posts.ts +++ b/src/payload/collections/Posts.ts @@ -45,6 +45,67 @@ export const Posts: CollectionConfig = { }; }, }, + hooks: { + afterChange: [ + async ({ doc, req }) => { + // Run index sync asynchronously to not block the CMS save operation + setTimeout(async () => { + try { + const { upsertProductVector, deleteProductVector } = await import('../../lib/qdrant'); + + // Check if post is published + if (doc._status !== 'published') { + await deleteProductVector(`post_${doc.id}`); + req.payload.logger.info(`Removed drafted post ${doc.slug} from Qdrant`); + } else { + // Serialize payload + const contentText = [ + `Blog-Artikel: ${doc.title}`, + doc.excerpt ? `Zusammenfassung: ${doc.excerpt}` : '', + doc.category ? `Kategorie: ${doc.category}` : '', + ] + .filter(Boolean) + .join('\n'); + + const payload = { + type: 'knowledge', + content: contentText, + data: { + title: doc.title, + slug: doc.slug, + }, + }; + + await upsertProductVector(`post_${doc.id}`, contentText, payload); + req.payload.logger.info(`Upserted post ${doc.slug} to Qdrant`); + } + } catch (error) { + req.payload.logger.error({ + msg: 'Error syncing post to Qdrant', + err: error, + postId: doc.id, + }); + } + }, 0); + return doc; + }, + ], + afterDelete: [ + async ({ id, req }) => { + try { + const { deleteProductVector } = await import('../../lib/qdrant'); + await deleteProductVector(`post_${id}`); + req.payload.logger.info(`Deleted post ${id} from Qdrant`); + } catch (error) { + req.payload.logger.error({ + msg: 'Error deleting post from Qdrant', + err: error, + postId: id, + }); + } + }, + ], + }, fields: [ { name: 'title', diff --git a/src/payload/collections/Products.ts b/src/payload/collections/Products.ts index 78387f39..0c6dadd3 100644 --- a/src/payload/collections/Products.ts +++ b/src/payload/collections/Products.ts @@ -37,6 +37,51 @@ export const Products: CollectionConfig = { }; }, }, + hooks: { + afterChange: [ + async ({ doc, req, operation }) => { + // Run index sync asynchronously to not block the CMS save operation + setTimeout(async () => { + try { + const { upsertProductVector, deleteProductVector } = await import('../../lib/qdrant'); + + // Check if product is published + if (doc._status !== 'published') { + await deleteProductVector(doc.id); + req.payload.logger.info(`Removed drafted product ${doc.sku} from Qdrant`); + } else { + // Serialize payload + const contentText = `${doc.title} - SKU: ${doc.sku}\n${doc.description || ''}`; + const payload = { + id: doc.id, + title: doc.title, + sku: doc.sku, + slug: doc.slug, + description: doc.description, + featuredImage: doc.featuredImage, // usually just ID or URL depending on depth + }; + await upsertProductVector(doc.id, contentText, payload); + req.payload.logger.info(`Upserted product ${doc.sku} to Qdrant`); + } + } catch (error) { + req.payload.logger.error({ msg: 'Error syncing product to Qdrant', err: error, productId: doc.id }); + } + }, 0); + return doc; + }, + ], + afterDelete: [ + async ({ id, req }) => { + try { + const { deleteProductVector } = await import('../../lib/qdrant'); + await deleteProductVector(id as string | number); + req.payload.logger.info(`Deleted product ${id} from Qdrant`); + } catch (error) { + req.payload.logger.error({ msg: 'Error deleting product from Qdrant', err: error, productId: id }); + } + }, + ], + }, fields: [ { name: 'title', diff --git a/src/scripts/error.png b/src/scripts/error.png new file mode 100644 index 00000000..f9674c9b Binary files /dev/null and b/src/scripts/error.png differ diff --git a/src/scripts/ingest-pdf.ts b/src/scripts/ingest-pdf.ts new file mode 100644 index 00000000..138a9c07 --- /dev/null +++ b/src/scripts/ingest-pdf.ts @@ -0,0 +1,64 @@ +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +import 'dotenv/config'; + +// Override Qdrant URL for local script execution outside docker +process.env.QDRANT_URL = process.env.QDRANT_URL || 'http://localhost:6333'; + +import { upsertProductVector } from '../lib/qdrant'; + +// Ingests the extracted Kabelhandbuch text into Qdrant as distinct knowledge topics. +async function ingestPDF(txtPath: string) { + if (!fs.existsSync(txtPath)) { + console.error(`File not found: ${txtPath}`); + process.exit(1); + } + + try { + const text = fs.readFileSync(txtPath, 'utf8'); + + // Simple sentence/paragraph chunking + // We split by standard paragraph breaks (double newline) or large content blocks. + const chunks = text + .split(/\n\s*\n/) + .map((c) => c.trim()) + .filter((c) => c.length > 50); + + console.log(`Extracted ${text.length} characters from PDF.`); + console.log(`Generated ${chunks.length} chunks for vector ingestion.\n`); + + for (let i = 0; i < chunks.length; i++) { + // We limit chuck sizes to ensure Openrouter embedding models don't timeout/fail, + // stringing multiple paragraphs if they are short, or cutting them if too long. + // For baseline, we'll index every chunk individually mapped as 'knowledge' with a unique ID + + const chunkText = chunks[i]; + + // Generate a synthetic ID that won't collide with Payload Product IDs + // Qdrant strictly requires UUID or unsigned int. + const syntheticId = crypto.randomUUID(); + + const payloadData = { + type: 'knowledge', // Custom flag to differentiate from 'product' + title: `Kabelhandbuch Wissen - Bereich ${i + 1}`, + content: chunkText, + source: 'Kabelhandbuch KLZ.pdf', + }; + + // Use the existing upsert function since it just embeds the text and stores the payload + await upsertProductVector(syntheticId, chunkText, payloadData); + console.log(`✅ Upserted chunk ${i + 1}/${chunks.length}`); + } + + console.log('🎉 PDF Ingestion Complete!'); + process.exit(0); + } catch (err) { + console.error('Failed to parse PDF:', err); + process.exit(1); + } +} + +// Run mapping +const targetTxt = '/Users/marcmintel/Downloads/kabelhandbuch.txt'; +ingestPDF(targetTxt); diff --git a/test-chat2.mjs b/test-chat2.mjs new file mode 100644 index 00000000..c574cff9 --- /dev/null +++ b/test-chat2.mjs @@ -0,0 +1,20 @@ +import fetch from 'node-fetch'; + +async function test() { + const messages = [ + { role: 'user', content: 'Ich will einen Windpark bauen' } + ]; + + console.log('Sending message:', messages[0].content); + + const res = await fetch('http://localhost:3000/api/ai-search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages }) + }); + + const data = await res.json(); + console.log('\nAI Response:', data); +} + +test().catch(console.error); diff --git a/test-simple.mjs b/test-simple.mjs new file mode 100644 index 00000000..20a28cf3 --- /dev/null +++ b/test-simple.mjs @@ -0,0 +1,16 @@ +import { generateText } from 'ai'; +import { createOpenAI } from '@ai-sdk/openai'; + +const openrouter = createOpenAI({ + baseURL: 'https://openrouter.ai/api/v1', + apiKey: process.env.OPENROUTER_API_KEY, +}); + +async function run() { + const { text } = await generateText({ + model: openrouter('mistralai/mistral-large-2407'), + prompt: 'Hello world! Reply in one word.', + }); + console.log('Result:', text); +} +run(); diff --git a/turbo.json b/turbo.json index 1b1f9510..ff0dbc4e 100644 --- a/turbo.json +++ b/turbo.json @@ -43,10 +43,6 @@ "check:spell": { "inputs": ["content/**/*.{md,mdx}", "app/**/*.tsx", "components/**/*.tsx", "cspell.json"], "outputs": [] - }, - "check:mdx": { - "inputs": ["content/**/*.{md,mdx}", "scripts/validate-mdx.mjs"], - "outputs": [] } } }