diff --git a/.gitattributes b/.gitattributes index 738b0bf315..897cfae915 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,11 +1,5 @@ licenses/** linguist-generated -# Chat UI bundles are built by the separate eddi-chat-ui repo and committed here -# so the backend ships a matching widget. Reviewing minified output line by line -# is meaningless -- review the source in eddi-chat-ui instead. -src/main/resources/META-INF/resources/scripts/js/chat-ui.*.js linguist-generated -src/main/resources/META-INF/resources/scripts/css/chat-ui.*.css linguist-generated -src/main/resources/META-INF/resources/assets/** linguist-generated # Install scripts -- enforce correct line endings per platform *.sh text eol=lf @@ -16,3 +10,8 @@ src/main/resources/META-INF/resources/assets/** linguist-generated # execute on Linux and macOS ("bad interpreter"), which would silently disarm the # force-push guard for everyone who activated it. .githooks/** text eol=lf + +# Lockfiles are LF in the index already; pin it so a Windows checkout with +# core.autocrlf cannot churn them. CRLF normalisation at commit time is what made +# a committed chat bundle differ byte-for-byte from a fresh build of the same name. +ui/*/package-lock.json text eol=lf diff --git a/.github/codeql/codeql-ui.yml b/.github/codeql/codeql-ui.yml new file mode 100644 index 0000000000..92676f9411 --- /dev/null +++ b/.github/codeql/codeql-ui.yml @@ -0,0 +1,15 @@ +# CodeQL configuration for the javascript-typescript analysis (ci.yml `codeql-ui` and the +# scheduled codeql.yml). It scans the application code that ships in the image: the Manager +# and Chat UI sources. +# +# Everything else is out of scope on purpose, not overlooked: +# - ui/manager/public/mockServiceWorker.js is MSW's generated dev worker; the Maven build +# excludes it from the jar. +# - ui/manager/scripts, e2e/, .ds-sync/ and .design-sync/ are CI and developer tooling. +# - Without `paths` the JavaScript extractor also reads YAML across the repository, so it +# graded backend files such as helm/eddi/values.yaml, which the java-kotlin analysis and +# the manifest tests already own. +name: EDDI UI +paths: + - ui/manager/src + - ui/chat/src diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fd2104b792..de71a160d0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -93,6 +93,98 @@ updates: commit-message: prefix: "chore(docker)" + # The two frontends under ui/. They ran Renovate in their own repos (automerge + # of minor/patch once CI was green); the Renovate app is not installed on this + # repository, and two bots on one repo is the one clearly wrong option. + - package-ecosystem: npm + directory: /ui/manager + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + labels: + - dependencies + - chore + # Let a release settle before proposing it; security updates are not delayed. + cooldown: + default-days: 3 + semver-major-days: 14 + groups: + # One PR for every open advisory instead of one per package. + security-updates: + applies-to: security-updates + patterns: + - "*" + minor-and-patch: + patterns: + - "*" + update-types: + - minor + - patch + ignore: + # Carried over from the Manager's renovate.json: v8 folds react-router-dom + # into react-router, so an automated major breaks every import. Minor and + # patch bumps stay enabled — that is how a security fix arrives. + - dependency-name: "react-router*" + update-types: ["version-update:semver-major"] + + - package-ecosystem: npm + directory: /ui/chat + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 3 + labels: + - dependencies + - chore + # Let a release settle before proposing it; security updates are not delayed. + cooldown: + default-days: 3 + semver-major-days: 14 + groups: + # One PR for every open advisory instead of one per package. + security-updates: + applies-to: security-updates + patterns: + - "*" + minor-and-patch: + patterns: + - "*" + update-types: + - minor + - patch + ignore: + # Same as the Manager: a react-router v8 major needs a manual migration. + - dependency-name: "react-router*" + update-types: ["version-update:semver-major"] + + # The Manager's design-sync tooling (ui/manager/.ds-sync) is its own npm project with + # its own lockfile, and Dependency Review and Trivy scan it like any other: a stale + # advisory there fails every PR. Not shipped, so version updates are rare and small. + - package-ecosystem: npm + directory: /ui/manager/.ds-sync + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 2 + labels: + - dependencies + - chore + cooldown: + default-days: 3 + semver-major-days: 14 + groups: + security-updates: + applies-to: security-updates + patterns: + - "*" + minor-and-patch: + patterns: + - "*" + update-types: + - minor + - patch + # GitHub Actions versions - package-ecosystem: github-actions directory: / diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb5ab2df46..b2889312b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,10 @@ jobs: code: ${{ steps.result.outputs.code }} scripts: ${{ steps.result.outputs.scripts }} operator_docs: ${{ steps.result.outputs.operator_docs }} + ui: ${{ steps.result.outputs.ui }} + backend: ${{ steps.result.outputs.backend }} + # JSON list consumed by e2e-fullstack's matrix; see "Resolve E2E databases". + databases: ${{ steps.dbs.outputs.databases }} steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -61,10 +65,14 @@ jobs: - 'src/**' - 'pom.xml' - '.github/workflows/**' + - '.github/codeql/**' - '**/Dockerfile*' - '**/.dockerignore' - 'docker-compose*.yml' - 'mcp-sidecar/**' + # The Manager and Chat UIs ship inside the jar, so a UI-only change + # must still produce, E2E-test and publish an image. + - 'ui/**' - 'k8s/**' - 'helm/**' - 'mvnw*' @@ -75,22 +83,54 @@ jobs: - '.trivyignore' - '.gitleaksignore' - 'licenses/**' - # Graded by unit tests, so they are build inputs rather than prose: + # Everything a Java test reads: `code` without the frontends, plus files + # that only tests read and that change nothing in the image. On a pull + # request Build & Test and Integration Tests gate on this instead of + # `code`, so a change confined to ui/ does not run the Java suite; on + # push and on tags they still gate on `code`, so nothing is published + # untested — and a change to only the test-read files below runs Build & + # Test and nothing that builds or publishes an image. + # BuildQualityGatesTest keeps this equal to `code` minus 'ui/**', plus + # the last four entries. + backend: + - 'src/**' + - 'pom.xml' + - '.github/workflows/**' + - '.github/codeql/**' + - '**/Dockerfile*' + - '**/.dockerignore' + - 'docker-compose*.yml' + - 'mcp-sidecar/**' + - 'k8s/**' + - 'helm/**' + - 'mvnw*' + - '.mvn/**' + - 'checkstyle.xml' + - 'eclipse-formatter.xml' + - 'mise.toml' + - '.trivyignore' + - '.gitleaksignore' + - 'licenses/**' + # The UI markdown DocumentationLinksTest and DocumentationAccuracyTest walk. + - 'ui/**/*.md' + # Graded by unit tests, so they are test inputs rather than prose: # ComposeStackTest parses README.md's compose commands, and - # BuildQualityGatesTest reads AGENTS.md 4.7 to check it still - # describes the Checkstyle import gate's real scope. Outside this - # filter, a PR that changes only one of them sets code=false and - # skips the very test that grades it, so a stale compose command or - # a gate description that no longer matches the pom merges with - # nothing having evaluated it. + # BuildQualityGatesTest reads AGENTS.md 4.7 to check it still describes + # the Checkstyle import gate's real scope. Outside this filter, a PR that + # changes only one of them skips the very test that grades it. - 'README.md' - 'AGENTS.md' # Same shape one file along: BuildQualityGatesTest grades both the - # pre-push hook's recorded 100755 mode and the remote its recovery - # fetch queries. .githooks/** is in `scripts` as well, but that job - # only lints shell — it cannot run the unit test that owns the - # hook's logic, so a hook-only PR would change it ungraded. + # pre-push hook's recorded 100755 mode and the remote its recovery fetch + # queries. .githooks/** is in `scripts` as well, but that job only lints + # shell — it cannot run the unit test that owns the hook's logic. - '.githooks/**' + # And again: BuildQualityGatesTest reads dependabot.yml to check that the + # Docker ecosystems it declares stay in step with base-image-check.yml's + # skip logic. Dependabot's own validation check grades the file's schema, + # not that contract, so without this entry a PR that changes only + # dependabot.yml skips the one test that owns it. + - '.github/dependabot.yml' # The installers are the README's headline `curl | bash` path and were # in NO filter at all, so a PR touching only them skipped the whole # pipeline — and a skipped required check still satisfies branch @@ -122,6 +162,13 @@ jobs: - 'docs/kubernetes.md' - 'README.md' - 'docs/security.md' + # Gates the ui-* jobs. The workflow is listed so that a CI-only change + # still runs the UI jobs it may have broken. pom.xml is deliberately not: + # the ui-* jobs never run Maven, and the pom's UI wiring (node.version, + # the frontend plugin, copy-ui-bundles) is exercised by Build Image. + ui: + - 'ui/**' + - '.github/workflows/ci.yml' - name: Resolve id: result @@ -136,16 +183,35 @@ jobs: echo "code=true" >> "$GITHUB_OUTPUT" echo "scripts=true" >> "$GITHUB_OUTPUT" echo "operator_docs=true" >> "$GITHUB_OUTPUT" + echo "ui=true" >> "$GITHUB_OUTPUT" + echo "backend=true" >> "$GITHUB_OUTPUT" else echo "code=${{ steps.filter.outputs.code }}" >> "$GITHUB_OUTPUT" echo "scripts=${{ steps.filter.outputs.scripts }}" >> "$GITHUB_OUTPUT" echo "operator_docs=${{ steps.filter.outputs.operator_docs }}" >> "$GITHUB_OUTPUT" + echo "ui=${{ steps.filter.outputs.ui }}" >> "$GITHUB_OUTPUT" + echo "backend=${{ steps.filter.outputs.backend }}" >> "$GITHUB_OUTPUT" + fi + + - name: Resolve E2E databases + id: dbs + # The E2E matrix is data, not a job-level `if:`. `jobs..if` is evaluated + # before the matrix expands and cannot see `matrix`, so a condition there + # would not filter legs at all. PRs run MongoDB only; main and tags run both + # stores before anything is published. + run: | + if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then + echo 'databases=["mongodb"]' >> "$GITHUB_OUTPUT" + else + echo 'databases=["mongodb","postgres"]' >> "$GITHUB_OUTPUT" fi - name: Log result run: | echo "### 📊 Change Detection" >> "$GITHUB_STEP_SUMMARY" echo "- Code changed: **${{ steps.result.outputs.code }}**" >> "$GITHUB_STEP_SUMMARY" + echo "- UI changed: **${{ steps.result.outputs.ui }}**" >> "$GITHUB_STEP_SUMMARY" + echo "- Backend changed: **${{ steps.result.outputs.backend }}**" >> "$GITHUB_STEP_SUMMARY" if [[ "$GITHUB_REF" == refs/tags/* ]]; then echo "- 🏷️ Tag push — full pipeline forced regardless of changed paths" >> "$GITHUB_STEP_SUMMARY" fi @@ -495,19 +561,30 @@ jobs: name: Build & Test runs-on: ubuntu-latest needs: detect-changes - # operator_docs as well as code. DeploymentManifestsTest asserts things about + # backend or operator_docs on a pull request; code on push and on tags. + # + # operator_docs: DeploymentManifestsTest asserts things about # docs/kubernetes.md and README.md — that they route `kubectl apply -k` # through create-secrets.sh, name ContainerCreating, and do not advertise the - # mutable tag — and neither file is in the `code` filter, so the one change - # those guards could not see was a change to the files they guard. + # mutable tag — so the one change those guards could not see was a change to + # the files they guard. + # + # backend, not code, on a pull request: a change confined to ui/ cannot fail a + # Java test (the resource tests read stand-in shells), so a Manager, Chat or + # npm-Dependabot PR does not pay for the ~20k-test suite. On push and on tags + # this job still gates on `code`, so docker's `needs` see every published + # commit tested. # - # Nothing downstream widens with it: integration-test and trivy-scan still - # gate on `code` alone, and docker `needs` all three, so a docs-only change - # runs the tests and publishes nothing. sbom and preflight-check carry the - # same `code` condition explicitly for that reason. (codeql has no condition - # at all — it is deliberately unconditional for OpenSSF Scorecard; see its - # own header.) - if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.operator_docs == 'true' + # Nothing downstream widens with operator_docs: integration-test and + # trivy-scan still gate on `code`, and docker `needs` all three, so a docs-only + # change runs the tests and publishes nothing. sbom and preflight-check carry + # the same `code` condition explicitly for that reason. (codeql has no + # condition at all — it is deliberately unconditional for OpenSSF Scorecard; + # see its own header.) + if: >- + needs.detect-changes.outputs.backend == 'true' || + needs.detect-changes.outputs.operator_docs == 'true' || + (github.event_name != 'pull_request' && needs.detect-changes.outputs.code == 'true') steps: - name: Checkout @@ -521,7 +598,21 @@ jobs: cache: maven - name: Build and test - run: ./mvnw clean test -B + # -DskipUi=true is belt and braces: `test` stops before prepare-package, where + # the UIs are built. They have their own ui-* jobs, and the image is built + # once, by Build Image. The resource tests that read the SPA shells use the + # stand-ins under src/test/resources. + run: ./mvnw clean test -DskipUi=true -B + + # Integration Tests restores this instead of running the unit tests again: the + # merged 90/80 coverage gate needs their execution data, not a second run. + - name: Upload unit-test coverage data + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: unit-test-coverage-data + path: target/jacoco.exec + if-no-files-found: error + retention-days: 7 - name: Upload test results if: always() @@ -605,7 +696,9 @@ jobs: name: Integration Tests runs-on: ubuntu-latest needs: [detect-changes, build-and-test] - if: needs.detect-changes.outputs.code == 'true' + # Same split as build-and-test: on a pull request only a backend change runs the + # Testcontainers suite; on push and on tags any code change does. + if: needs.detect-changes.outputs.code == 'true' && (github.event_name != 'pull_request' || needs.detect-changes.outputs.backend == 'true') steps: - name: Checkout @@ -618,8 +711,25 @@ jobs: java-version: ${{ env.JAVA_VERSION }} cache: maven + # Build & Test already ran the unit tests on this commit (this job `needs` it). + # Their coverage data and reports are restored instead of re-running ~20k tests, + # so the merged coverage gate below sees exactly what it saw before. + - name: Download unit-test coverage data + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: unit-test-coverage-data + path: target + + - name: Download unit-test reports + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: test-results + path: target/surefire-reports + - name: Run integration tests - run: ./mvnw verify -DskipITs=false -Dquarkus.bootstrap.effective-model-builder=true -B + # -DskipUi=true: the ITs are API-level, and ContainerBaseIT's image needs no UI. + # -DskipUTs=true: see the two steps above. + run: ./mvnw verify -DskipITs=false -DskipUTs=true -DskipUi=true -Dquarkus.bootstrap.effective-model-builder=true -B env: MAVEN_OPTS: "--enable-native-access=ALL-UNNAMED" @@ -754,13 +864,210 @@ jobs: # Test is the job that owns that verdict (it deliberately does NOT pass # these flags); repeating it here only delays CodeQL and duplicates the # failure. This job compiles for SAST, nothing more. - run: ./mvnw compile -DskipTests -Dformatter.skip=true -Dcheckstyle.skip=true -B --no-transfer-progress + # + # -DskipUi=true is load-bearing here: this job is ungated, so without it every + # docs-only PR would run the full npm build of both UIs just to compile Java. + run: ./mvnw compile -DskipTests -DskipUi=true -Dformatter.skip=true -Dcheckstyle.skip=true -B --no-transfer-progress - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: category: "/language:java-kotlin" + # ─── Job 2b-ui: CodeQL SAST for the UIs ──────────────────────── + # ui/manager and ui/chat are TypeScript. A separate job rather than a language + # matrix on `codeql` above, deliberately: a matrix renames that job's check from + # `CodeQL Analysis` to `CodeQL Analysis (java-kotlin)`, and `CodeQL Analysis` is a + # REQUIRED status check on main — it would never report again and every PR would + # wait on it forever. Ungated for the same Scorecard reason as `codeql`, and so + # that code scanning never sees this category missing from a PR's analyses. + codeql-ui: + name: CodeQL Analysis (UI) + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + languages: javascript-typescript + build-mode: none + queries: security-extended + # Scans the shipped UI sources only; the file says what is left out and why. + config-file: ./.github/codeql/codeql-ui.yml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + category: "/language:javascript-typescript" + + # ─── Job 2f: UI jobs ─────────────────────────────────────────── + # The Manager's and the Chat UI's own CI, carried over from their former repos + # (EDDI-Manager ci-cd.yml + the `UI E2E (MSW)` tier of its e2e.yml), as three + # parallel jobs so a Chat failure does not wait behind the Manager's tests. + # Their production builds are not repeated here: Build Image builds both UIs + # through Maven and checks the output. The tiers that need a running backend + # are in e2e-fullstack, against that image. + ui-manager-checks: + name: UI Manager Checks + runs-on: ubuntu-latest + needs: detect-changes + if: needs.detect-changes.outputs.ui == 'true' + defaults: + run: + working-directory: ui/manager + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Node 20 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + cache: npm + cache-dependency-path: | + ui/manager/package-lock.json + + - name: Install + run: npm ci + + - name: Audit production dependencies + run: npm run audit:prod + + - name: Lint + run: npm run lint + + # Fails on any drift between the code and the locale files: a key passed to + # t() that en.json lacks, a key en.json has that a locale lacks, or a leftover + # key in a locale. See ui/manager/scripts/check-i18n.mjs. + - name: i18n drift check + run: npm run i18n:check + + - name: Type check + run: npm run typecheck + + # Includes openapi-contract.test.ts, which checks the MSW handlers against the + # committed OpenAPI snapshot. e2e-fullstack checks that snapshot against the + # backend built from this same commit. + - name: Unit tests with coverage + run: npx vitest run --coverage + + - name: Upload coverage report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ui-manager-coverage + path: ui/manager/coverage/ + retention-days: 14 + + ui-manager-e2e: + name: UI Manager E2E (MSW) + runs-on: ubuntu-latest + needs: detect-changes + if: needs.detect-changes.outputs.ui == 'true' + defaults: + run: + working-directory: ui/manager + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Node 20 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + cache: npm + cache-dependency-path: | + ui/manager/package-lock.json + + - name: Install + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + # Two workers: the config's `workers: 1` on CI exists for the backend tiers, + # whose specs share state in serial groups. Every MSW test gets its own mocked + # page, and 234 tests measured 5.4 and 5.8 minutes at two workers (both green, + # no flakes) against 13.1 at one. + - name: UI E2E (MSW, no backend) + run: npm run test:e2e -- --workers=2 + + - name: Upload Playwright report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ui-manager-e2e-report + path: ui/manager/playwright-report/ + retention-days: 14 + + ui-chat: + name: UI Chat + runs-on: ubuntu-latest + needs: detect-changes + if: needs.detect-changes.outputs.ui == 'true' + defaults: + run: + working-directory: ui/chat + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Node 20 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + cache: npm + cache-dependency-path: | + ui/chat/package-lock.json + + - name: Install + run: npm ci + + - name: Type check + run: npm run typecheck + + - name: Unit tests + run: npm test + + # ─── Job 2g: UI Gate ─────────────────────────────────────────── + # docker must not publish an image whose UI failed its checks — but a bare + # `needs` on the ui-* jobs would SKIP docker on every backend-only push, because + # a skipped dependency skips its dependents. This always runs, and fails only + # when the UI changed and one of its jobs did not succeed. It is the one UI + # check to make required. + ui-gate: + name: UI Gate + runs-on: ubuntu-latest + needs: [detect-changes, ui-manager-checks, ui-manager-e2e, ui-chat] + if: always() + steps: + - name: Require UI success only when the UI changed + env: + DETECT: ${{ needs.detect-changes.result }} + CHANGED: ${{ needs.detect-changes.outputs.ui }} + CHECKS: ${{ needs.ui-manager-checks.result }} + E2E: ${{ needs.ui-manager-e2e.result }} + CHAT: ${{ needs.ui-chat.result }} + run: | + if [ "$DETECT" != "success" ]; then + echo "::error::detect-changes=$DETECT, so whether the UI changed is unknown" + exit 1 + fi + if [ "$CHANGED" = "true" ]; then + for result in "ui-manager-checks=$CHECKS" "ui-manager-e2e=$E2E" "ui-chat=$CHAT"; do + if [ "${result#*=}" != "success" ]; then + echo "::error::the UI changed but $result" + exit 1 + fi + done + fi + echo "ui changed=$CHANGED, checks=$CHECKS, e2e=$E2E, chat=$CHAT — gate satisfied" + # ─── Job 2c: Trivy Filesystem Scan ───────────────────────────── trivy-scan: name: Trivy Filesystem Scan @@ -892,49 +1199,70 @@ jobs: java-version: ${{ env.JAVA_VERSION }} cache: maven - - name: Generate CycloneDX SBOM + - name: Set up Node 20 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + + - name: Generate CycloneDX SBOM (Maven) run: ./mvnw org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeBom -DskipTests -B --no-transfer-progress + - name: Generate CycloneDX SBOMs (npm) + # Both UIs are built into the jar, so their npm production dependencies are part + # of the shipped supply chain — and cyclonedx-maven-plugin inventories Maven only, + # so without this the published SBOM described just over half the artifact. + # + # --package-lock-only reads package-lock.json instead of node_modules, so this + # needs no `npm ci` and cannot execute install scripts. --omit dev matches what + # Vite actually bundles into dist/; ui/manager/.ds-sync is design-sync tooling + # that never reaches the image, so it is deliberately not inventoried here. + run: | + set -euo pipefail + for ui in manager chat; do + npx --yes @cyclonedx/cyclonedx-npm@6.0.1 \ + --package-lock-only \ + --omit dev \ + --output-format JSON \ + --output-file "target/bom-ui-$ui.json" \ + "ui/$ui/package.json" + done + - name: Upload SBOM + # Three CycloneDX documents rather than one merged file: merging across ecosystems + # needs cyclonedx-cli, a GitHub-release binary we would have to fetch unverified, + # and a hand-rolled merge of metadata/bom-refs/dependency graphs is exactly the + # kind of thing that yields a plausible but invalid BOM. Each file here is a valid + # document on its own, and every consumer we target (Dependency-Track, Red Hat + # certification, grype) ingests a set. uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: sbom-cyclonedx - path: target/bom.json + path: | + target/bom.json + target/bom-ui-manager.json + target/bom-ui-chat.json retention-days: 90 - # ─── Job 3: Docker Build & Push ───────────────────────────────── - docker: - name: Docker Build & Push + # ─── Job 3a: Build Image ──────────────────────────────────────── + # Builds the application — both UIs included — and the Docker image exactly + # once per run, and hands the image on as an artifact: + # e2e-fullstack boots it and drives the Manager against it + # preflight-check certifies it on pull requests + # docker publishes it on push, so the published bytes are the tested bytes + # Needs only detect-changes, deliberately in parallel with build-and-test so the + # E2E tier does not queue behind the unit suite; docker still needs both. Uses no + # secrets, so pull requests from forks get the full E2E run. + build-image: + name: Build Image runs-on: ubuntu-latest - # Every gate that must hold before anything is published is a `needs`: - # build-and-test unit tests - # integration-test `mvnw verify` — the ONLY job that runs the JaCoCo - # 90/80 coverage gate, so without it the threshold could - # never block a ship - # trivy-scan blocking filesystem CVE scan (exit-code 1) - # codeql SAST build + analysis - # gitleaks secret scanning - needs: [detect-changes, build-and-test, integration-test, trivy-scan, codeql, gitleaks] - # NOTE: deliberately no always(). always() cancelled the implicit - # needs-success gate, and the tag branch then short-circuited the explicit - # one — so a tag on a commit with failing tests still built, pushed, tagged - # :latest, cosign-signed and SLSA-attested. Tag pushes are kept unskippable - # by forcing detect-changes to report code=true for them instead. - if: > - github.event_name == 'push' && - github.repository == 'labsai/EDDI' && - (startsWith(github.ref, 'refs/tags/') || - !contains(github.event.head_commit.message || '', '[skip docker]')) - + needs: detect-changes + if: needs.detect-changes.outputs.code == 'true' outputs: primary-tag: ${{ steps.meta.outputs.primary-tag }} is-release: ${{ steps.meta.outputs.is-release }} is-stable: ${{ steps.meta.outputs.is-stable }} - - permissions: - contents: read # checkout + build - id-token: write # Sigstore cosign keyless signing (OIDC → Fulcio) - attestations: write # SLSA provenance attestation + minor-tag: ${{ steps.meta.outputs.minor-tag }} + major-tag: ${{ steps.meta.outputs.major-tag }} steps: - name: Checkout @@ -1031,37 +1359,554 @@ jobs: echo "- \`${DOCKER_IMAGE}:latest\`" >> "$GITHUB_STEP_SUMMARY" fi + # frontend-maven-plugin vendors its own Node, but its `npm ci` reads and writes + # the same ~/.npm cache this restores; without it every run downloaded both + # dependency trees cold. + - name: Set up Node 20 (npm cache for the UI build) + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + cache: npm + cache-dependency-path: | + ui/manager/package-lock.json + ui/chat/package-lock.json + - name: Build application - # STYLE_GATES_SKIP — see the note in the CodeQL job. This job is - # `needs: build-and-test`, so the style verdict for this exact commit is - # already in; re-running it here just adds ~2 minutes to every publish. - run: ./mvnw clean package -DskipTests -Dformatter.skip=true -Dcheckstyle.skip=true -Plicense-gen -B + # No -DskipUi: this is the build that ships, so it runs npm ci + npm run build + # for both UIs (frontend-maven-plugin vendors Node into ui/node) and copies + # their dist/ into the jar. `clean` matters: copy-resources never prunes, so + # an incremental build would package hashed assets from older builds. + # + # STYLE_GATES_SKIP — see the note in the CodeQL job. Build & Test owns the + # style verdict for this commit, and docker needs it before anything ships. + # + # store-schema-directory writes the OpenAPI document to target/openapi, where the + # openapi-snapshot job checks the Manager's snapshot without booting a backend. + run: ./mvnw clean package -DskipTests -Dformatter.skip=true -Dcheckstyle.skip=true -Plicense-gen -Dquarkus.smallrye-openapi.store-schema-directory=target/openapi -B + + - name: Verify the built and packaged UI + # What EDDI serves by exact name must exist: the three Manager shells and the + # Chat shell and bundle. /assets/ is served `immutable` for a year, which is + # only safe while every file there carries a content hash. And copy-resources + # never prunes, so without `clean` above every hashed chunk of every earlier + # build would ship too (reproduced locally: 913 packaged assets against 737 + # built) — the jar must carry exactly the assets Vite just emitted, and never + # the MSW dev worker. + run: | + set -euo pipefail + packaged=$(cd target/classes/META-INF/resources/assets && find . -type f | sort) + built=$(cd ui/manager/dist/assets && find . -type f | sort) + if [ "$packaged" != "$built" ]; then + echo "::error::target/classes/META-INF/resources/assets differs from ui/manager/dist/assets:" + diff <(echo "$built") <(echo "$packaged") || true + exit 1 + fi + if [ -e target/classes/META-INF/resources/mockServiceWorker.js ]; then + echo "::error::mockServiceWorker.js was packaged" + exit 1 + fi + for shell in manage welcome workforce; do + [ -f "ui/manager/dist/$shell.html" ] || { echo "::error::ui/manager/dist/$shell.html was not built"; exit 1; } + done + if [ -e ui/manager/dist/index.html ]; then + echo "::error::ui/manager/dist/index.html was built — index.html is the dev-server entry and must not be a build input" + exit 1 + fi + unhashed=$(echo "$built" | sed 's#^\./##' | grep -vE '^.+[.-][A-Za-z0-9_-]{8}\.[A-Za-z0-9]+$' || true) + if [ -n "$unhashed" ]; then + echo "::error::these assets carry no content hash, so the immutable cache header would pin a mutable file:" + echo "$unhashed" + exit 1 + fi + ls ui/chat/dist/chat.html ui/chat/dist/scripts/js/chat-ui.*.js ui/chat/dist/scripts/css/chat-ui.*.css > /dev/null + echo "packaged assets match the build: $(echo "$built" | wc -l) hashed files, four shells, the Chat bundle" + + - name: Upload the OpenAPI document + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: openapi-document + path: target/openapi/openapi.json + if-no-files-found: error + retention-days: 7 - name: Build Docker image - # PRIMARY_TAG arrives through env:, never through ${{ }} — see the note on - # RELEASE_TAG_PATTERN in "Compute Docker tags". + # PRIMARY_TAG arrives through env:, never through ${{ }} interpolation — see + # the note on RELEASE_TAG_PATTERN in "Compute Docker tags". + # + # The `version` label differs by event, exactly as the two builds this job + # replaces did: a pull request labels the bare pom version, which is what + # preflight-check certifies and asserts; a push labels the tag the image is + # published under. env: PRIMARY_TAG: ${{ steps.meta.outputs.primary-tag }} run: | - IS_RELEASE="${{ steps.meta.outputs.is-release }}" - IS_STABLE="${{ steps.meta.outputs.is-stable }}" - - TAGS="-t ${DOCKER_IMAGE}:${PRIMARY_TAG}" - if [[ "$IS_STABLE" == "true" ]]; then - TAGS="${TAGS} -t ${DOCKER_IMAGE}:${{ steps.meta.outputs.minor-tag }}" - TAGS="${TAGS} -t ${DOCKER_IMAGE}:${{ steps.meta.outputs.major-tag }}" - fi - if [[ "$IS_RELEASE" == "true" ]]; then - TAGS="${TAGS} -t ${DOCKER_IMAGE}:latest" + set -euo pipefail + if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then + LABEL_VERSION=$(grep -m1 '' pom.xml | cut -d'>' -f2 | cut -d'<' -f1) + else + LABEL_VERSION="$PRIMARY_TAG" fi - + echo "Building ${DOCKER_IMAGE}:ci with EDDI_VERSION=${LABEL_VERSION}" docker build \ - --build-arg EDDI_VERSION="${PRIMARY_TAG}" \ + --build-arg EDDI_VERSION="${LABEL_VERSION}" \ --build-arg EDDI_RELEASE="${{ github.run_number }}" \ -f src/main/docker/Dockerfile \ - ${TAGS} \ + -t "${DOCKER_IMAGE}:ci" \ . + - name: Export the image + run: | + set -euo pipefail + docker save "${DOCKER_IMAGE}:ci" | gzip -1 > eddi-ci.tar.gz + ls -lh eddi-ci.tar.gz + + - name: Upload the image + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: eddi-ci-image + path: eddi-ci.tar.gz + # Already gzipped; recompressing only costs time. + compression-level: 0 + if-no-files-found: error + # Not 1: re-running a Friday pipeline on Monday must still find it. + retention-days: 7 + + # ─── Job 3a-openapi: OpenAPI snapshot ─────────────────────────── + # The Manager's MSW handlers are checked against a committed snapshot of EDDI's + # operations (openapi-contract.test.ts). This compares that snapshot with the + # document the Quarkus build stored — seconds after Build Image, with no + # container — so an API change learns it must refresh the snapshot early. A job + # of its own, not a step in Build Image: a failing step there would withhold the + # image from every E2E tier. e2e-fullstack repeats the check against the running + # backend, which also guards the build-time and served documents agreeing. + openapi-snapshot: + name: OpenAPI Snapshot + runs-on: ubuntu-latest + needs: [detect-changes, build-image] + if: needs.detect-changes.outputs.code == 'true' + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Node 20 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + + - name: Download the OpenAPI document + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: openapi-document + path: target/openapi + + - name: Check the snapshot matches this build + id: openapi + working-directory: ui/manager + env: + OPENAPI_FILE: ${{ github.workspace }}/target/openapi/openapi.json + run: | + node scripts/refresh-openapi-operations.mjs + if ! git diff --exit-code -I '"eddiVersion"' -- src/test/mocks/openapi-operations.json; then + echo "::error::EDDI's API surface changed and ui/manager/src/test/mocks/openapi-operations.json is stale." + echo "::error::Download the openapi-operations-refreshed artifact from this run and commit it, or run the script as its header describes." + exit 1 + fi + echo "Snapshot operations match the OpenAPI document of this build." + + - name: Upload the regenerated OpenAPI snapshot + if: ${{ failure() && steps.openapi.outcome == 'failure' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: openapi-operations-refreshed + path: ui/manager/src/test/mocks/openapi-operations.json + retention-days: 14 + + # ─── Job 3a-e2e: Backend E2E ──────────────────────────────────── + # The Manager's API-integration and full-stack Playwright tiers, run against the + # image built from THIS commit — never a labsai/eddi:latest pulled from Docker + # Hub, which is what these tiers ran against while they lived in EDDI-Manager + # (a backend change could only break them after it had been published). + e2e-fullstack: + name: Backend E2E (${{ matrix.database }}) + runs-on: ubuntu-latest + needs: [detect-changes, build-image] + if: needs.detect-changes.outputs.code == 'true' + strategy: + fail-fast: false + matrix: + database: ${{ fromJSON(needs.detect-changes.outputs.databases) }} + env: + # Read by the compose files as `image: ${EDDI_IMAGE:-labsai/eddi:latest}`. + EDDI_IMAGE: labsai/eddi:ci + DATABASE: ${{ matrix.database }} + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Download the image + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: eddi-ci-image + + - name: Load the image + run: | + set -euo pipefail + gunzip -c eddi-ci.tar.gz | docker load + rm eddi-ci.tar.gz + docker image inspect "$EDDI_IMAGE" --format 'Testing image {{.Id}} built {{.Created}}' + + - name: Select the compose file + run: | + case "$DATABASE" in + mongodb) file=docker-compose.integration.yml ;; + postgres) file=docker-compose.integration-postgres.yml ;; + *) echo "::error::no compose file for database '$DATABASE'"; exit 1 ;; + esac + echo "E2E_COMPOSE_FILE=ui/manager/$file" >> "$GITHUB_ENV" + + - name: Start backend infrastructure + run: docker compose -f "$E2E_COMPOSE_FILE" up -d --wait + timeout-minutes: 5 + + - name: Wait for EDDI health + id: health + run: | + for i in $(seq 1 60); do + if curl -sf http://localhost:7070/q/health/live | grep -q '"UP"'; then + echo "Backend is healthy after $((i * 2))s" + exit 0 + fi + sleep 2 + done + echo "::error::Backend did not become healthy within 120s" + docker compose -f "$E2E_COMPOSE_FILE" logs eddi + exit 1 + + - name: Verify the shipped shells + # The Playwright tiers below drive the shipped bundle, but a browser only + # fetches what the pages it visits need. This follows every same-origin dependency + # of the four shells — the hashed entry chunks, /manage/__auth_config__.js + # (served by Quarkus, not Vite), icons, the loader logo, the chat fonts in + # chat.html's inline - - - - -
- - diff --git a/src/main/resources/META-INF/resources/img/loading-indicator.svg b/src/main/resources/META-INF/resources/img/loading-indicator.svg deleted file mode 100644 index 0d95b951df..0000000000 --- a/src/main/resources/META-INF/resources/img/loading-indicator.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/manage.html b/src/main/resources/META-INF/resources/manage.html deleted file mode 100644 index 2e775fc309..0000000000 --- a/src/main/resources/META-INF/resources/manage.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - EDDI Manager - - - - - - -
-
- EDDI -
-
Loading EDDI Manager…
-
-
- - diff --git a/src/main/resources/META-INF/resources/scripts/css/chat-ui.DZ5NqRHZ.css b/src/main/resources/META-INF/resources/scripts/css/chat-ui.DZ5NqRHZ.css deleted file mode 100644 index fda567f0e6..0000000000 --- a/src/main/resources/META-INF/resources/scripts/css/chat-ui.DZ5NqRHZ.css +++ /dev/null @@ -1 +0,0 @@ -:root,[data-theme=dark]{--chat-bg: #0a0a0a;--chat-surface: #141414;--chat-surface-raised: #1e1e1e;--chat-text: #f1f1f1;--chat-text-muted: #999;--chat-text-accent: #cba135;--chat-accent: #113b92;--chat-accent-hover: #1a50b5;--chat-accent-soft: rgba(17, 59, 146, .15);--chat-agent-bg: #1e1e1e;--chat-agent-border: #2a2a2a;--chat-agent-text: #f1f1f1;--chat-user-bg: #113b92;--chat-user-text: #ffffff;--chat-input-bg: #1a1a1a;--chat-input-border: #333;--chat-input-focus: #113b92;--chat-input-text: #f1f1f1;--chat-input-placeholder: #666;--chat-qr-bg: rgba(17, 59, 146, .18);--chat-qr-border: rgba(17, 59, 146, .4);--chat-qr-text: #7eaaff;--chat-qr-hover: rgba(17, 59, 146, .35);--chat-border: #222;--chat-radius: 16px;--chat-radius-sm: 8px;--chat-font: "Noto Sans", system-ui, -apple-system, sans-serif;--chat-shadow: 0 2px 12px rgba(0, 0, 0, .4);--chat-scrollbar-thumb: #444;--chat-scrollbar-track: transparent;--chat-warning: #d9822b}[data-theme=light]{--chat-bg: #f5f5f5;--chat-surface: #ffffff;--chat-surface-raised: #fafafa;--chat-text: #1a1a1a;--chat-text-muted: #666;--chat-text-accent: #8b6914;--chat-accent: #113b92;--chat-accent-hover: #0d2d6b;--chat-accent-soft: rgba(17, 59, 146, .08);--chat-agent-bg: #ffffff;--chat-agent-border: #e0e0e0;--chat-agent-text: #1a1a1a;--chat-user-bg: #113b92;--chat-user-text: #ffffff;--chat-input-bg: #ffffff;--chat-input-border: #d0d0d0;--chat-input-focus: #113b92;--chat-input-text: #1a1a1a;--chat-input-placeholder: #999;--chat-qr-bg: rgba(17, 59, 146, .06);--chat-qr-border: rgba(17, 59, 146, .25);--chat-qr-text: #113b92;--chat-qr-hover: rgba(17, 59, 146, .14);--chat-border: #e0e0e0;--chat-shadow: 0 2px 12px rgba(0, 0, 0, .08);--chat-scrollbar-thumb: #ccc;--chat-scrollbar-track: transparent;--chat-warning: #a85a12}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;overflow:hidden}body{background-color:var(--chat-bg);color:var(--chat-text);font-family:var(--chat-font);font-weight:400;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{color:var(--chat-text-accent)}a:hover{opacity:.85}.chat-root{display:flex;flex-direction:column;height:100%;max-width:860px;margin:0 auto;background-color:var(--chat-bg)}.chat-header{display:flex;align-items:center;gap:12px;padding:12px 20px;border-bottom:1px solid var(--chat-border);background:var(--chat-surface);flex-shrink:0}.chat-header__branding{display:flex;align-items:center;gap:10px;flex:1;min-width:0}.chat-header__logo{height:36px;object-fit:contain}.chat-header__agent-name{font-size:.85rem;font-weight:500;color:var(--chat-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-header__title{flex:1;font-size:1rem;font-weight:500;color:var(--chat-text);letter-spacing:.02em}.chat-header__actions{display:flex;align-items:center;gap:6px}.chat-header__btn{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:var(--chat-radius-sm);background:transparent;color:var(--chat-text-muted);cursor:pointer;transition:background .15s,color .15s;font-size:1.1rem}.chat-header__btn:hover{background:var(--chat-accent-soft);color:var(--chat-text)}.chat-input-area{flex-shrink:0}.chat-actions{display:flex;align-items:center;justify-content:space-between;padding:4px 16px 0;border-top:1px solid var(--chat-border)}.chat-actions__left,.chat-actions__right{display:flex;align-items:center;gap:4px}.chat-actions__btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border:none;border-radius:var(--chat-radius-sm);background:transparent;color:var(--chat-text-muted);cursor:pointer;transition:background .15s,color .15s,opacity .15s;font-size:1rem}.chat-actions__btn:hover:not(:disabled){background:var(--chat-accent-soft);color:var(--chat-text)}.chat-actions__btn:disabled{cursor:not-allowed}.chat-messages{flex:1;overflow-y:auto;padding:16px 12px;display:flex;flex-direction:column;gap:4px}.chat-messages::-webkit-scrollbar{width:5px}.chat-messages::-webkit-scrollbar-thumb{background:var(--chat-scrollbar-thumb);border-radius:4px}.chat-messages::-webkit-scrollbar-track{background:var(--chat-scrollbar-track)}.message{display:flex;gap:10px;padding:4px 8px;max-width:85%;animation:msgIn .25s ease-out}@keyframes msgIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.message--user{align-self:flex-end;flex-direction:row-reverse}.message--agent{align-self:flex-start}.message__avatar{flex-shrink:0;width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:600}.message--user .message__avatar{background:var(--chat-accent);color:var(--chat-user-text)}.message--agent .message__avatar{background:var(--chat-surface-raised);color:var(--chat-text-accent);border:1px solid var(--chat-agent-border)}.message__bubble{border-radius:var(--chat-radius);padding:10px 16px;font-size:.925rem;line-height:1.6;word-break:break-word}.message--user .message__bubble{background:var(--chat-user-bg);color:var(--chat-user-text);border-bottom-right-radius:4px}.message--agent .message__bubble{background:var(--chat-agent-bg);color:var(--chat-agent-text);border:1px solid var(--chat-agent-border);border-bottom-left-radius:4px}.message__bubble .markdown-body{font-size:inherit;line-height:inherit;color:inherit}.message__bubble .markdown-body p{margin:.4em 0}.message__bubble .markdown-body p:first-child{margin-top:0}.message__bubble .markdown-body p:last-child{margin-bottom:0}.message__bubble .markdown-body pre{background:var(--chat-surface);border:1px solid var(--chat-border);border-radius:var(--chat-radius-sm);padding:12px;overflow-x:auto;margin:8px 0;font-size:.85rem}.message__bubble .markdown-body code{background:var(--chat-surface);border-radius:3px;padding:1px 5px;font-size:.85em;font-family:Noto Sans Mono,ui-monospace,monospace}.message__bubble .markdown-body pre code{background:none;padding:0}.message__bubble .markdown-body ul,.message__bubble .markdown-body ol{padding-left:1.4em;margin:.4em 0}.message__bubble .markdown-body table{border-collapse:collapse;margin:8px 0;width:100%}.message__bubble .markdown-body th,.message__bubble .markdown-body td{border:1px solid var(--chat-border);padding:6px 10px;text-align:left;font-size:.85rem}.message__bubble .markdown-body th{background:var(--chat-surface-raised);font-weight:500}.message__bubble .markdown-body blockquote{border-left:3px solid var(--chat-accent);padding-left:12px;margin:8px 0;color:var(--chat-text-muted)}.indicator{display:flex;gap:10px;padding:4px 8px;align-self:flex-start;animation:msgIn .25s ease-out}.indicator__avatar{flex-shrink:0;width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:600;background:var(--chat-surface-raised);color:var(--chat-text-accent);border:1px solid var(--chat-agent-border)}.indicator__bubble{background:var(--chat-agent-bg);border:1px solid var(--chat-agent-border);border-radius:var(--chat-radius);border-bottom-left-radius:4px;padding:12px 18px;display:flex;align-items:center;gap:8px}.indicator__dots{display:flex;align-items:center;gap:4px}.indicator__dot{width:7px;height:7px;border-radius:50%;background:var(--chat-text-muted);animation:bounce 1.2s infinite}.indicator__dot:nth-child(2){animation-delay:.15s}.indicator__dot:nth-child(3){animation-delay:.3s}@keyframes bounce{0%,60%,to{transform:translateY(0)}30%{transform:translateY(-6px)}}.indicator__thinking{display:flex;align-items:center;gap:8px;color:var(--chat-text-muted);font-size:.85rem;font-style:italic}.indicator__brain{font-size:1rem;animation:pulse 1.5s ease-in-out infinite}@keyframes pulse{0%,to{opacity:.4}50%{opacity:1}}.quick-replies{display:flex;flex-wrap:wrap;gap:8px;padding:8px 20px 12px;animation:msgIn .2s ease-out}.quick-replies__btn{padding:8px 16px;font-size:.875rem;font-family:var(--chat-font);font-weight:400;border:1px solid var(--chat-qr-border);border-radius:20px;background:var(--chat-qr-bg);color:var(--chat-qr-text);cursor:pointer;transition:background .15s,border-color .15s,transform .1s;white-space:nowrap}.quick-replies__btn:hover{background:var(--chat-qr-hover);border-color:var(--chat-qr-text)}.quick-replies__btn:active{transform:scale(.97)}.chat-input{display:flex;align-items:flex-end;gap:10px;padding:12px 16px 16px;border-top:1px solid var(--chat-border);background:var(--chat-surface)}.chat-input__textarea{flex:1;resize:none;min-height:44px;max-height:150px;padding:10px 14px;font-size:.925rem;font-family:var(--chat-font);line-height:1.5;color:var(--chat-input-text);background:var(--chat-input-bg);border:1px solid var(--chat-input-border);border-radius:14px;outline:none;transition:border-color .15s,box-shadow .15s}.chat-input__textarea::placeholder{color:var(--chat-input-placeholder)}.chat-input__textarea:focus{border-color:var(--chat-input-focus);box-shadow:0 0 0 2px var(--chat-accent-soft)}.chat-input__send{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:44px;height:44px;border:none;border-radius:14px;font-size:1.2rem;cursor:pointer;transition:background .15s,transform .1s}.chat-input__send--active{background:var(--chat-accent);color:var(--chat-user-text)}.chat-input__send--active:hover{background:var(--chat-accent-hover)}.chat-input__send--active:active{transform:scale(.95)}.chat-input__send--disabled{background:var(--chat-surface-raised);color:var(--chat-text-muted);cursor:not-allowed}.chat-input__spinner{width:18px;height:18px;border:2px solid transparent;border-top-color:currentColor;border-radius:50%;animation:spin .6s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.chat-input__attach{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:var(--chat-radius-sm);background:transparent;color:var(--chat-text-muted);cursor:pointer;transition:background .15s,color .15s;font-size:1.1rem}.chat-input__attach:hover:not(:disabled){background:var(--chat-accent-soft);color:var(--chat-text)}.chat-input__attach:disabled{opacity:.35;cursor:not-allowed}.chat-input__secret-toggle{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:var(--chat-radius-sm);background:transparent;color:var(--chat-text-muted);cursor:pointer;transition:background .15s,color .15s;font-size:1.1rem}.chat-input__secret-toggle:hover{background:var(--chat-accent-soft);color:var(--chat-text)}.chat-input__secret-toggle--active{background:var(--chat-accent-soft);color:var(--chat-accent)}.chat-input__secret-wrapper{flex:1;position:relative;display:flex;align-items:center}.chat-input__secret-field{flex:1;padding:10px 40px 10px 14px;font-size:.925rem;font-family:var(--chat-font);line-height:1.5;color:var(--chat-input-text);background:var(--chat-input-bg);border:1px solid var(--chat-accent);border-radius:14px;outline:none;transition:border-color .15s,box-shadow .15s;height:44px}.chat-input__secret-field::placeholder{color:var(--chat-input-placeholder)}.chat-input__secret-field:focus{border-color:var(--chat-accent);box-shadow:0 0 0 2px var(--chat-accent-soft)}.chat-input__eye-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:1rem;color:var(--chat-text-muted);padding:4px;line-height:1;transition:opacity .15s}.chat-input__eye-toggle:hover{opacity:.7}.secret-input{padding:12px 16px 16px;border-top:1px solid var(--chat-border);background:var(--chat-surface);animation:msgIn .25s ease-out}.secret-input__label{display:block;font-size:.85rem;font-weight:500;color:var(--chat-accent);margin-bottom:8px;letter-spacing:.02em}.secret-input__row{display:flex;align-items:center;gap:10px}.secret-input__field-wrapper{flex:1;position:relative;display:flex;align-items:center}.secret-input__field{flex:1;padding:10px 40px 10px 14px;font-size:.925rem;font-family:var(--chat-font);line-height:1.5;color:var(--chat-input-text);background:var(--chat-input-bg);border:1px solid var(--chat-accent);border-radius:14px;outline:none;transition:border-color .15s,box-shadow .15s;height:44px}.secret-input__field::placeholder{color:var(--chat-input-placeholder)}.secret-input__field:focus{border-color:var(--chat-accent);box-shadow:0 0 0 2px var(--chat-accent-soft)}.secret-input__eye-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:1rem;color:var(--chat-text-muted);padding:4px;line-height:1;transition:opacity .15s}.secret-input__eye-toggle:hover{opacity:.7}.scroll-to-bottom{position:absolute;bottom:90px;left:50%;transform:translate(-50%);width:38px;height:38px;border:1px solid var(--chat-border);border-radius:50%;background:var(--chat-surface);color:var(--chat-text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:1.1rem;box-shadow:var(--chat-shadow);transition:background .15s,color .15s;z-index:10;animation:fadeIn .2s ease-out}.scroll-to-bottom:hover{background:var(--chat-surface-raised);color:var(--chat-text)}@keyframes fadeIn{0%{opacity:0;transform:translate(-50%) translateY(8px)}to{opacity:1;transform:translate(-50%) translateY(0)}}.chat-ended{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;border-top:1px solid var(--chat-border);background:var(--chat-surface);text-align:center}.chat-ended__label{font-size:.9rem;color:var(--chat-text-muted);font-weight:500}.chat-ended__restart{padding:8px 24px;font-size:.875rem;font-family:var(--chat-font);font-weight:500;border:1px solid var(--chat-accent);border-radius:20px;background:var(--chat-accent-soft);color:var(--chat-qr-text);cursor:pointer;transition:background .15s,transform .1s}.chat-ended__restart:hover{background:var(--chat-accent);color:var(--chat-user-text)}.chat-ended__restart:active{transform:scale(.97)}.chat-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;color:var(--chat-text-muted);text-align:center;padding:40px 20px}.chat-empty__icon{font-size:3rem;opacity:.3}.chat-empty__text{font-size:.95rem}@media(min-width:1440px){.chat-root{max-width:960px}}@media(min-width:1920px){.chat-root{max-width:1080px}.message__bubble{font-size:1rem}}@media(max-width:1280px){.chat-root{max-width:800px}}@media(max-width:1024px){.chat-root{max-width:720px}.message{max-width:88%}}@media(max-width:768px){.chat-root{max-width:100%}.chat-header{padding:10px 14px}.chat-header__logo{height:28px}.chat-messages{padding:12px 8px}.message{max-width:90%}.chat-input{padding:10px 12px 14px}.chat-actions{padding:4px 12px 0}.quick-replies{padding:8px 12px 10px}}@media(max-width:640px){.chat-header{padding:8px 12px;gap:8px}.chat-header__logo{height:26px}.chat-header__agent-name{font-size:.8rem}.chat-header__btn{width:32px;height:32px;font-size:1rem}.message{gap:8px;max-width:92%}.message__bubble{padding:9px 13px;font-size:.9rem;border-radius:14px}.message--user .message__bubble{border-bottom-right-radius:4px}.message--agent .message__bubble{border-bottom-left-radius:4px}.chat-input__textarea{min-height:40px;padding:9px 12px;font-size:.9rem;border-radius:12px}.chat-input__send{width:40px;height:40px;border-radius:12px}.quick-replies{gap:6px;padding:6px 10px 8px}.quick-replies__btn{font-size:.82rem;padding:6px 14px}.chat-actions__btn{width:28px;height:28px;font-size:.9rem}.scroll-to-bottom{width:34px;height:34px;bottom:80px}}@media(max-width:480px){.chat-header{padding:8px 10px;gap:6px}.chat-header__logo{height:24px}.chat-header__agent-name{font-size:.75rem;max-width:120px}.chat-messages{padding:8px 6px;gap:2px}.message{gap:6px;padding:3px 4px;max-width:94%}.message__avatar{width:26px;height:26px;font-size:.65rem}.message__bubble{padding:8px 12px;font-size:.875rem;line-height:1.5}.message__bubble .markdown-body pre{padding:8px;font-size:.78rem}.message__bubble .markdown-body th,.message__bubble .markdown-body td{padding:4px 6px;font-size:.78rem}.chat-input{padding:8px 8px 12px;gap:8px}.chat-input__textarea{min-height:38px;padding:8px 10px;font-size:.875rem;border-radius:10px}.chat-input__send{width:38px;height:38px;font-size:1.1rem;border-radius:10px}.chat-actions{padding:2px 8px 0}.quick-replies{padding:6px 8px 8px;gap:5px}.quick-replies__btn{font-size:.8rem;padding:5px 12px}.chat-ended{padding:14px;gap:10px}.chat-ended__restart{padding:6px 20px;font-size:.82rem}.scroll-to-bottom{width:32px;height:32px;bottom:70px;font-size:1rem}}@media(max-width:360px){.chat-header__logo{height:20px}.chat-header__agent-name{font-size:.7rem;max-width:80px}.chat-header__btn{width:28px;height:28px;font-size:.9rem}.message__avatar{width:22px;height:22px;font-size:.55rem}.message__bubble{padding:6px 10px;font-size:.82rem}.quick-replies__btn{font-size:.75rem;padding:4px 10px;border-radius:16px}}@media(hover:none)and (pointer:coarse){.chat-header__btn,.chat-actions__btn{min-width:44px;min-height:44px}.quick-replies__btn{min-height:36px}.chat-attachments__remove{min-width:36px;min-height:36px}}@media(max-height:500px)and (orientation:landscape){.chat-header{padding:6px 12px}.chat-header__logo{height:22px}.chat-messages{padding:6px 8px}.chat-input{padding:6px 10px 8px}.quick-replies{padding:4px 10px 6px}.chat-actions{padding:2px 10px 0}}@supports (padding: env(safe-area-inset-bottom)){.chat-input{padding-bottom:calc(16px + env(safe-area-inset-bottom))}.chat-ended{padding-bottom:calc(20px + env(safe-area-inset-bottom))}@media(max-width:480px){.chat-input{padding-bottom:calc(12px + env(safe-area-inset-bottom))}}}.chat-input-wrapper{display:flex;flex-direction:column;gap:6px}.chat-attachments{display:flex;flex-wrap:wrap;gap:6px;padding:0 12px;max-height:5.5rem;overflow-y:auto}.chat-attachments__chip{display:inline-flex;align-items:center;flex-wrap:wrap;gap:2px 6px;max-width:100%;padding:4px 6px 4px 10px;border:1px solid var(--chat-input-border);border-radius:14px;background:var(--chat-input-bg);color:var(--chat-text);font-size:.82rem;line-height:1.3}.chat-attachments__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chat-attachments__remove{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;border-radius:50%;background:transparent;color:var(--chat-text-muted);font-size:1rem;line-height:1;cursor:pointer}.chat-attachments__remove:hover,.chat-attachments__remove:focus-visible{background:var(--chat-accent-soft);color:var(--chat-text)}.chat-attachments__chip--warn{border-color:var(--chat-text-accent)}.chat-attachments__warn{flex:1 1 100%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--chat-text-accent);font-size:.75rem}.chat-attachments__chip--uploading{color:var(--chat-text-muted);border-style:dashed;border-color:var(--chat-text-muted);opacity:.75}.chat-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0}@media(prefers-reduced-motion:reduce){.indicator__brain,.indicator__dot{animation:none}}.paused-card{margin:8px 12px;padding:12px 14px;border:1px solid var(--chat-qr-border, var(--chat-border));border-left:3px solid var(--chat-accent);border-radius:10px;background:var(--chat-agent-bg);color:var(--chat-agent-text)}.paused-card__head{display:flex;align-items:center;gap:8px;margin-bottom:6px}.paused-card__icon{font-size:1rem}.paused-card__title{font-weight:600;font-size:.92rem}.paused-card__text{margin:0;font-size:.9rem;line-height:1.45}.paused-card__tools{margin:8px 0 0;padding:0;list-style:none;display:flex;flex-wrap:wrap;gap:6px}.paused-card__tool{padding:2px 8px;border-radius:10px;background:var(--chat-accent-soft, rgba(127, 127, 127, .15));font-family:var(--chat-font-mono, monospace);font-size:.8rem}.paused-card__deadline{margin:8px 0 0;color:var(--chat-text-muted);font-size:.82rem}.paused-card__actions{margin-top:10px}.paused-card__cancel{padding:5px 12px;border:1px solid var(--chat-border);border-radius:8px;background:transparent;color:var(--chat-text-muted);font-size:.82rem;cursor:pointer}.paused-card__cancel:hover:not(:disabled),.paused-card__cancel:focus-visible:not(:disabled){border-color:var(--chat-accent);color:var(--chat-text)}.paused-card__cancel:disabled{opacity:.5;cursor:not-allowed}.recovery-banner{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin:0 12px 8px;padding:10px 12px;border:1px solid var(--chat-border);border-left:3px solid var(--chat-warning);border-radius:10px;background:var(--chat-surface)}.recovery-banner__text{color:var(--chat-text);font-size:.86rem}.recovery-banner__actions{display:flex;gap:6px}.recovery-banner__btn{padding:4px 10px;border:1px solid var(--chat-border);border-radius:8px;background:transparent;color:var(--chat-text);font-size:.82rem;cursor:pointer}.recovery-banner__btn:hover:not(:disabled),.recovery-banner__btn:focus-visible:not(:disabled){border-color:var(--chat-accent)}.recovery-banner__btn:disabled{opacity:.5;cursor:not-allowed}.agent-picker{flex:1;overflow-y:auto;padding:32px 24px;max-width:640px;width:100%;margin:0 auto}.agent-picker__title{margin:0 0 8px;font-size:1.25rem;font-weight:600;color:var(--chat-text)}.agent-picker__hint{margin:0 0 20px;font-size:.9rem;color:var(--chat-text-muted)}.agent-picker__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.agent-picker__item{display:flex;flex-direction:column;gap:4px;padding:12px 14px;border:1px solid var(--chat-border);border-radius:var(--chat-radius-sm);background:var(--chat-surface);color:var(--chat-text);text-decoration:none}.agent-picker__item:hover,.agent-picker__item:focus-visible{border-color:var(--chat-accent);background:var(--chat-accent-soft)}.agent-picker__name{font-weight:600}.agent-picker__description{font-size:.85rem;color:var(--chat-text-muted)} diff --git a/src/main/resources/META-INF/resources/scripts/js/chat-ui.DHWeOytM.js b/src/main/resources/META-INF/resources/scripts/js/chat-ui.DHWeOytM.js deleted file mode 100644 index 1ffdc50593..0000000000 --- a/src/main/resources/META-INF/resources/scripts/js/chat-ui.DHWeOytM.js +++ /dev/null @@ -1,169 +0,0 @@ -var ny=Object.defineProperty;var ay=(e,n,u)=>n in e?ny(e,n,{enumerable:!0,configurable:!0,writable:!0,value:u}):e[n]=u;var uf=(e,n,u)=>ay(e,typeof n!="symbol"?n+"":n,u);(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))i(s);new MutationObserver(s=>{for(const c of s)if(c.type==="childList")for(const o of c.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function u(s){const c={};return s.integrity&&(c.integrity=s.integrity),s.referrerPolicy&&(c.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?c.credentials="include":s.crossOrigin==="anonymous"?c.credentials="omit":c.credentials="same-origin",c}function i(s){if(s.ep)return;s.ep=!0;const c=u(s);fetch(s.href,c)}})();function EE(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var rf={exports:{}},pi={};/** - * @license React - * react-jsx-runtime.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Mp;function uy(){if(Mp)return pi;Mp=1;var e=Symbol.for("react.transitional.element"),n=Symbol.for("react.fragment");function u(i,s,c){var o=null;if(c!==void 0&&(o=""+c),s.key!==void 0&&(o=""+s.key),"key"in s){c={};for(var d in s)d!=="key"&&(c[d]=s[d])}else c=s;return s=c.ref,{$$typeof:e,type:i,key:o,ref:s!==void 0?s:null,props:c}}return pi.Fragment=n,pi.jsx=u,pi.jsxs=u,pi}var wp;function ry(){return wp||(wp=1,rf.exports=uy()),rf.exports}var Q=ry(),lf={exports:{}},Oe={};/** - * @license React - * react.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Bp;function iy(){if(Bp)return Oe;Bp=1;var e=Symbol.for("react.transitional.element"),n=Symbol.for("react.portal"),u=Symbol.for("react.fragment"),i=Symbol.for("react.strict_mode"),s=Symbol.for("react.profiler"),c=Symbol.for("react.consumer"),o=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),E=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),g=Symbol.for("react.lazy"),T=Symbol.for("react.activity"),_=Symbol.iterator;function y(D){return D===null||typeof D!="object"?null:(D=_&&D[_]||D["@@iterator"],typeof D=="function"?D:null)}var R={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},I=Object.assign,w={};function L(D,Z,C){this.props=D,this.context=Z,this.refs=w,this.updater=C||R}L.prototype.isReactComponent={},L.prototype.setState=function(D,Z){if(typeof D!="object"&&typeof D!="function"&&D!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,D,Z,"setState")},L.prototype.forceUpdate=function(D){this.updater.enqueueForceUpdate(this,D,"forceUpdate")};function X(){}X.prototype=L.prototype;function q(D,Z,C){this.props=D,this.context=Z,this.refs=w,this.updater=C||R}var he=q.prototype=new X;he.constructor=q,I(he,L.prototype),he.isPureReactComponent=!0;var de=Array.isArray;function j(){}var ae={H:null,A:null,T:null,S:null},ye=Object.prototype.hasOwnProperty;function Se(D,Z,C){var F=C.ref;return{$$typeof:e,type:D,key:Z,ref:F!==void 0?F:null,props:C}}function Y(D,Z){return Se(D.type,Z,D.props)}function pe(D){return typeof D=="object"&&D!==null&&D.$$typeof===e}function me(D){var Z={"=":"=0",":":"=2"};return"$"+D.replace(/[=:]/g,function(C){return Z[C]})}var ce=/\/+/g;function oe(D,Z){return typeof D=="object"&&D!==null&&D.key!=null?me(""+D.key):Z.toString(36)}function Ee(D){switch(D.status){case"fulfilled":return D.value;case"rejected":throw D.reason;default:switch(typeof D.status=="string"?D.then(j,j):(D.status="pending",D.then(function(Z){D.status==="pending"&&(D.status="fulfilled",D.value=Z)},function(Z){D.status==="pending"&&(D.status="rejected",D.reason=Z)})),D.status){case"fulfilled":return D.value;case"rejected":throw D.reason}}throw D}function G(D,Z,C,F,J){var se=typeof D;(se==="undefined"||se==="boolean")&&(D=null);var xe=!1;if(D===null)xe=!0;else switch(se){case"bigint":case"string":case"number":xe=!0;break;case"object":switch(D.$$typeof){case e:case n:xe=!0;break;case g:return xe=D._init,G(xe(D._payload),Z,C,F,J)}}if(xe)return J=J(D),xe=F===""?"."+oe(D,0):F,de(J)?(C="",xe!=null&&(C=xe.replace(ce,"$&/")+"/"),G(J,Z,C,"",function(nt){return nt})):J!=null&&(pe(J)&&(J=Y(J,C+(J.key==null||D&&D.key===J.key?"":(""+J.key).replace(ce,"$&/")+"/")+xe)),Z.push(J)),1;xe=0;var Ae=F===""?".":F+":";if(de(D))for(var ve=0;ve>>1,x=G[ue];if(0>>1;ues(C,Te))Fs(J,C)?(G[ue]=J,G[F]=Te,ue=F):(G[ue]=C,G[Z]=Te,ue=Z);else if(Fs(J,Te))G[ue]=J,G[F]=Te,ue=F;else break e}}return re}function s(G,re){var Te=G.sortIndex-re.sortIndex;return Te!==0?Te:G.id-re.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var c=performance;e.unstable_now=function(){return c.now()}}else{var o=Date,d=o.now();e.unstable_now=function(){return o.now()-d}}var E=[],p=[],g=1,T=null,_=3,y=!1,R=!1,I=!1,w=!1,L=typeof setTimeout=="function"?setTimeout:null,X=typeof clearTimeout=="function"?clearTimeout:null,q=typeof setImmediate<"u"?setImmediate:null;function he(G){for(var re=u(p);re!==null;){if(re.callback===null)i(p);else if(re.startTime<=G)i(p),re.sortIndex=re.expirationTime,n(E,re);else break;re=u(p)}}function de(G){if(I=!1,he(G),!R)if(u(E)!==null)R=!0,j||(j=!0,me());else{var re=u(p);re!==null&&Ee(de,re.startTime-G)}}var j=!1,ae=-1,ye=5,Se=-1;function Y(){return w?!0:!(e.unstable_now()-SeG&&Y());){var ue=T.callback;if(typeof ue=="function"){T.callback=null,_=T.priorityLevel;var x=ue(T.expirationTime<=G);if(G=e.unstable_now(),typeof x=="function"){T.callback=x,he(G),re=!0;break t}T===u(E)&&i(E),he(G)}else i(E);T=u(E)}if(T!==null)re=!0;else{var D=u(p);D!==null&&Ee(de,D.startTime-G),re=!1}}break e}finally{T=null,_=Te,y=!1}re=void 0}}finally{re?me():j=!1}}}var me;if(typeof q=="function")me=function(){q(pe)};else if(typeof MessageChannel<"u"){var ce=new MessageChannel,oe=ce.port2;ce.port1.onmessage=pe,me=function(){oe.postMessage(null)}}else me=function(){L(pe,0)};function Ee(G,re){ae=L(function(){G(e.unstable_now())},re)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(G){G.callback=null},e.unstable_forceFrameRate=function(G){0>G||125ue?(G.sortIndex=Te,n(p,G),u(E)===null&&G===u(p)&&(I?(X(ae),ae=-1):I=!0,Ee(de,Te-ue))):(G.sortIndex=x,n(E,G),R||y||(R=!0,j||(j=!0,me()))),G},e.unstable_shouldYield=Y,e.unstable_wrapCallback=function(G){var re=_;return function(){var Te=_;_=re;try{return G.apply(this,arguments)}finally{_=Te}}}})(of)),of}var Pp;function sy(){return Pp||(Pp=1,cf.exports=ly()),cf.exports}var ff={exports:{}},It={};/** - * @license React - * react-dom.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Fp;function cy(){if(Fp)return It;Fp=1;var e=ih();function n(E){var p="https://react.dev/errors/"+E;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(n){console.error(n)}}return e(),ff.exports=cy(),ff.exports}/** - * @license React - * react-dom-client.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Yp;function fy(){if(Yp)return Ei;Yp=1;var e=sy(),n=ih(),u=oy();function i(t){var a="https://react.dev/errors/"+t;if(1x||(t.current=ue[x],ue[x]=null,x--)}function C(t,a){x++,ue[x]=t.current,t.current=a}var F=D(null),J=D(null),se=D(null),xe=D(null);function Ae(t,a){switch(C(se,a),C(J,t),C(F,null),a.nodeType){case 9:case 11:t=(t=a.documentElement)&&(t=t.namespaceURI)?ap(t):0;break;default:if(t=a.tagName,a=a.namespaceURI)a=ap(a),t=up(a,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}Z(F),C(F,t)}function ve(){Z(F),Z(J),Z(se)}function nt(t){t.memoizedState!==null&&C(xe,t);var a=F.current,r=up(a,t.type);a!==r&&(C(J,t),C(F,r))}function We(t){J.current===t&&(Z(F),Z(J)),xe.current===t&&(Z(xe),fi._currentValue=Te)}var Ft,Ja;function St(t){if(Ft===void 0)try{throw Error()}catch(r){var a=r.stack.trim().match(/\n( *(at )?)/);Ft=a&&a[1]||"",Ja=-1)":-1f||v[l]!==P[f]){var W=` -`+v[l].replace(" at new "," at ");return t.displayName&&W.includes("")&&(W=W.replace("",t.displayName)),W}while(1<=l&&0<=f);break}}}finally{Vn=!1,Error.prepareStackTrace=r}return(r=t?t.displayName||t.name:"")?St(r):""}function we(t,a){switch(t.tag){case 26:case 27:case 5:return St(t.type);case 16:return St("Lazy");case 13:return t.child!==a&&a!==null?St("Suspense Fallback"):St("Suspense");case 19:return St("SuspenseList");case 0:case 15:return Xn(t.type,!1);case 11:return Xn(t.type.render,!1);case 1:return Xn(t.type,!0);case 31:return St("Activity");default:return""}}function zt(t){try{var a="",r=null;do a+=we(t,r),r=t,t=t.return;while(t);return a}catch(l){return` -Error generating stack: `+l.message+` -`+l.stack}}var Cn=Object.prototype.hasOwnProperty,Bn=e.unstable_scheduleCallback,Kn=e.unstable_cancelCallback,Yt=e.unstable_shouldYield,Cu=e.unstable_requestPaint,Ut=e.unstable_now,ee=e.unstable_getCurrentPriorityLevel,fe=e.unstable_ImmediatePriority,Re=e.unstable_UserBlockingPriority,ke=e.unstable_NormalPriority,Qe=e.unstable_LowPriority,jt=e.unstable_IdlePriority,Zn=e.log,Nn=e.unstable_setDisableYieldValue,xn=null,yt=null;function st(t){if(typeof Zn=="function"&&Nn(t),yt&&typeof yt.setStrictMode=="function")try{yt.setStrictMode(xn,t)}catch{}}var mt=Math.clz32?Math.clz32:YT,Rn=Math.log,zT=Math.LN2;function YT(t){return t>>>=0,t===0?32:31-(Rn(t)/zT|0)|0}var Qi=256,Vi=262144,Xi=4194304;function Wa(t){var a=t&42;if(a!==0)return a;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Ki(t,a,r){var l=t.pendingLanes;if(l===0)return 0;var f=0,h=t.suspendedLanes,b=t.pingedLanes;t=t.warmLanes;var S=l&134217727;return S!==0?(l=S&~h,l!==0?f=Wa(l):(b&=S,b!==0?f=Wa(b):r||(r=S&~t,r!==0&&(f=Wa(r))))):(S=l&~h,S!==0?f=Wa(S):b!==0?f=Wa(b):r||(r=l&~t,r!==0&&(f=Wa(r)))),f===0?0:a!==0&&a!==f&&(a&h)===0&&(h=f&-f,r=a&-a,h>=r||h===32&&(r&4194048)!==0)?a:f}function Cr(t,a){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&a)===0}function jT(t,a){switch(t){case 1:case 2:case 4:case 8:case 64:return a+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return a+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Hh(){var t=Xi;return Xi<<=1,(Xi&62914560)===0&&(Xi=4194304),t}function Gs(t){for(var a=[],r=0;31>r;r++)a.push(t);return a}function Nr(t,a){t.pendingLanes|=a,a!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function qT(t,a,r,l,f,h){var b=t.pendingLanes;t.pendingLanes=r,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=r,t.entangledLanes&=r,t.errorRecoveryDisabledLanes&=r,t.shellSuspendCounter=0;var S=t.entanglements,v=t.expirationTimes,P=t.hiddenUpdates;for(r=b&~r;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var ZT=/[\n"\\]/g;function cn(t){return t.replace(ZT,function(a){return"\\"+a.charCodeAt(0).toString(16)+" "})}function Js(t,a,r,l,f,h,b,S){t.name="",b!=null&&typeof b!="function"&&typeof b!="symbol"&&typeof b!="boolean"?t.type=b:t.removeAttribute("type"),a!=null?b==="number"?(a===0&&t.value===""||t.value!=a)&&(t.value=""+sn(a)):t.value!==""+sn(a)&&(t.value=""+sn(a)):b!=="submit"&&b!=="reset"||t.removeAttribute("value"),a!=null?Ws(t,b,sn(a)):r!=null?Ws(t,b,sn(r)):l!=null&&t.removeAttribute("value"),f==null&&h!=null&&(t.defaultChecked=!!h),f!=null&&(t.checked=f&&typeof f!="function"&&typeof f!="symbol"),S!=null&&typeof S!="function"&&typeof S!="symbol"&&typeof S!="boolean"?t.name=""+sn(S):t.removeAttribute("name")}function Jh(t,a,r,l,f,h,b,S){if(h!=null&&typeof h!="function"&&typeof h!="symbol"&&typeof h!="boolean"&&(t.type=h),a!=null||r!=null){if(!(h!=="submit"&&h!=="reset"||a!=null)){Zs(t);return}r=r!=null?""+sn(r):"",a=a!=null?""+sn(a):r,S||a===t.value||(t.value=a),t.defaultValue=a}l=l??f,l=typeof l!="function"&&typeof l!="symbol"&&!!l,t.checked=S?t.checked:!!l,t.defaultChecked=!!l,b!=null&&typeof b!="function"&&typeof b!="symbol"&&typeof b!="boolean"&&(t.name=b),Zs(t)}function Ws(t,a,r){a==="number"&&Wi(t.ownerDocument)===t||t.defaultValue===""+r||(t.defaultValue=""+r)}function Du(t,a,r,l){if(t=t.options,a){a={};for(var f=0;f"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ac=!1;if($n)try{var vr={};Object.defineProperty(vr,"passive",{get:function(){ac=!0}}),window.addEventListener("test",vr,vr),window.removeEventListener("test",vr,vr)}catch{ac=!1}var ya=null,uc=null,el=null;function ud(){if(el)return el;var t,a=uc,r=a.length,l,f="value"in ya?ya.value:ya.textContent,h=f.length;for(t=0;t=Ir),od=" ",fd=!1;function hd(t,a){switch(t){case"keyup":return Cb.indexOf(a.keyCode)!==-1;case"keydown":return a.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function dd(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var Mu=!1;function xb(t,a){switch(t){case"compositionend":return dd(a);case"keypress":return a.which!==32?null:(fd=!0,od);case"textInput":return t=a.data,t===od&&fd?null:t;default:return null}}function Rb(t,a){if(Mu)return t==="compositionend"||!cc&&hd(t,a)?(t=ud(),el=uc=ya=null,Mu=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(a.ctrlKey||a.altKey||a.metaKey)||a.ctrlKey&&a.altKey){if(a.char&&1=a)return{node:r,offset:a-t};t=l}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=_d(r)}}function Ad(t,a){return t&&a?t===a?!0:t&&t.nodeType===3?!1:a&&a.nodeType===3?Ad(t,a.parentNode):"contains"in t?t.contains(a):t.compareDocumentPosition?!!(t.compareDocumentPosition(a)&16):!1:!1}function Cd(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var a=Wi(t.document);a instanceof t.HTMLIFrameElement;){try{var r=typeof a.contentWindow.location.href=="string"}catch{r=!1}if(r)t=a.contentWindow;else break;a=Wi(t.document)}return a}function hc(t){var a=t&&t.nodeName&&t.nodeName.toLowerCase();return a&&(a==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||a==="textarea"||t.contentEditable==="true")}var wb=$n&&"documentMode"in document&&11>=document.documentMode,wu=null,dc=null,Br=null,mc=!1;function Nd(t,a,r){var l=r.window===r?r.document:r.nodeType===9?r:r.ownerDocument;mc||wu==null||wu!==Wi(l)||(l=wu,"selectionStart"in l&&hc(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),Br&&wr(Br,l)||(Br=l,l=Vl(dc,"onSelect"),0>=b,f-=b,Un=1<<32-mt(a)+f|r<Ie?(Fe=be,be=null):Fe=be.sibling;var qe=z(B,be,H[Ie],$);if(qe===null){be===null&&(be=Fe);break}t&&be&&qe.alternate===null&&a(B,be),k=h(qe,k,Ie),je===null?_e=qe:je.sibling=qe,je=qe,be=Fe}if(Ie===H.length)return r(B,be),ze&&ta(B,Ie),_e;if(be===null){for(;IeIe?(Fe=be,be=null):Fe=be.sibling;var Ya=z(B,be,qe.value,$);if(Ya===null){be===null&&(be=Fe);break}t&&be&&Ya.alternate===null&&a(B,be),k=h(Ya,k,Ie),je===null?_e=Ya:je.sibling=Ya,je=Ya,be=Fe}if(qe.done)return r(B,be),ze&&ta(B,Ie),_e;if(be===null){for(;!qe.done;Ie++,qe=H.next())qe=te(B,qe.value,$),qe!==null&&(k=h(qe,k,Ie),je===null?_e=qe:je.sibling=qe,je=qe);return ze&&ta(B,Ie),_e}for(be=l(be);!qe.done;Ie++,qe=H.next())qe=V(be,B,Ie,qe.value,$),qe!==null&&(t&&qe.alternate!==null&&be.delete(qe.key===null?Ie:qe.key),k=h(qe,k,Ie),je===null?_e=qe:je.sibling=qe,je=qe);return t&&be.forEach(function(ty){return a(B,ty)}),ze&&ta(B,Ie),_e}function Je(B,k,H,$){if(typeof H=="object"&&H!==null&&H.type===I&&H.key===null&&(H=H.props.children),typeof H=="object"&&H!==null){switch(H.$$typeof){case y:e:{for(var _e=H.key;k!==null;){if(k.key===_e){if(_e=H.type,_e===I){if(k.tag===7){r(B,k.sibling),$=f(k,H.props.children),$.return=B,B=$;break e}}else if(k.elementType===_e||typeof _e=="object"&&_e!==null&&_e.$$typeof===ye&&cu(_e)===k.type){r(B,k.sibling),$=f(k,H.props),Yr($,H),$.return=B,B=$;break e}r(B,k);break}else a(B,k);k=k.sibling}H.type===I?($=uu(H.props.children,B.mode,$,H.key),$.return=B,B=$):($=ol(H.type,H.key,H.props,null,B.mode,$),Yr($,H),$.return=B,B=$)}return b(B);case R:e:{for(_e=H.key;k!==null;){if(k.key===_e)if(k.tag===4&&k.stateNode.containerInfo===H.containerInfo&&k.stateNode.implementation===H.implementation){r(B,k.sibling),$=f(k,H.children||[]),$.return=B,B=$;break e}else{r(B,k);break}else a(B,k);k=k.sibling}$=_c(H,B.mode,$),$.return=B,B=$}return b(B);case ye:return H=cu(H),Je(B,k,H,$)}if(Ee(H))return ge(B,k,H,$);if(me(H)){if(_e=me(H),typeof _e!="function")throw Error(i(150));return H=_e.call(H),Ne(B,k,H,$)}if(typeof H.then=="function")return Je(B,k,gl(H),$);if(H.$$typeof===q)return Je(B,k,dl(B,H),$);Tl(B,H)}return typeof H=="string"&&H!==""||typeof H=="number"||typeof H=="bigint"?(H=""+H,k!==null&&k.tag===6?(r(B,k.sibling),$=f(k,H),$.return=B,B=$):(r(B,k),$=yc(H,B.mode,$),$.return=B,B=$),b(B)):r(B,k)}return function(B,k,H,$){try{zr=0;var _e=Je(B,k,H,$);return Qu=null,_e}catch(be){if(be===Gu||be===pl)throw be;var je=en(29,be,null,B.mode);return je.lanes=$,je.return=B,je}finally{}}}var fu=Xd(!0),Kd=Xd(!1),Na=!1;function kc(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Mc(t,a){t=t.updateQueue,a.updateQueue===t&&(a.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function xa(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function Ra(t,a,r){var l=t.updateQueue;if(l===null)return null;if(l=l.shared,(Ge&2)!==0){var f=l.pending;return f===null?a.next=a:(a.next=f.next,f.next=a),l.pending=a,a=cl(t),Id(t,null,r),a}return sl(t,l,a,r),cl(t)}function jr(t,a,r){if(a=a.updateQueue,a!==null&&(a=a.shared,(r&4194048)!==0)){var l=a.lanes;l&=t.pendingLanes,r|=l,a.lanes=r,Fh(t,r)}}function wc(t,a){var r=t.updateQueue,l=t.alternate;if(l!==null&&(l=l.updateQueue,r===l)){var f=null,h=null;if(r=r.firstBaseUpdate,r!==null){do{var b={lane:r.lane,tag:r.tag,payload:r.payload,callback:null,next:null};h===null?f=h=b:h=h.next=b,r=r.next}while(r!==null);h===null?f=h=a:h=h.next=a}else f=h=a;r={baseState:l.baseState,firstBaseUpdate:f,lastBaseUpdate:h,shared:l.shared,callbacks:l.callbacks},t.updateQueue=r;return}t=r.lastBaseUpdate,t===null?r.firstBaseUpdate=a:t.next=a,r.lastBaseUpdate=a}var Bc=!1;function qr(){if(Bc){var t=qu;if(t!==null)throw t}}function Gr(t,a,r,l){Bc=!1;var f=t.updateQueue;Na=!1;var h=f.firstBaseUpdate,b=f.lastBaseUpdate,S=f.shared.pending;if(S!==null){f.shared.pending=null;var v=S,P=v.next;v.next=null,b===null?h=P:b.next=P,b=v;var W=t.alternate;W!==null&&(W=W.updateQueue,S=W.lastBaseUpdate,S!==b&&(S===null?W.firstBaseUpdate=P:S.next=P,W.lastBaseUpdate=v))}if(h!==null){var te=f.baseState;b=0,W=P=v=null,S=h;do{var z=S.lane&-536870913,V=z!==S.lane;if(V?(Pe&z)===z:(l&z)===z){z!==0&&z===ju&&(Bc=!0),W!==null&&(W=W.next={lane:0,tag:S.tag,payload:S.payload,callback:null,next:null});e:{var ge=t,Ne=S;z=a;var Je=r;switch(Ne.tag){case 1:if(ge=Ne.payload,typeof ge=="function"){te=ge.call(Je,te,z);break e}te=ge;break e;case 3:ge.flags=ge.flags&-65537|128;case 0:if(ge=Ne.payload,z=typeof ge=="function"?ge.call(Je,te,z):ge,z==null)break e;te=T({},te,z);break e;case 2:Na=!0}}z=S.callback,z!==null&&(t.flags|=64,V&&(t.flags|=8192),V=f.callbacks,V===null?f.callbacks=[z]:V.push(z))}else V={lane:z,tag:S.tag,payload:S.payload,callback:S.callback,next:null},W===null?(P=W=V,v=te):W=W.next=V,b|=z;if(S=S.next,S===null){if(S=f.shared.pending,S===null)break;V=S,S=V.next,V.next=null,f.lastBaseUpdate=V,f.shared.pending=null}}while(!0);W===null&&(v=te),f.baseState=v,f.firstBaseUpdate=P,f.lastBaseUpdate=W,h===null&&(f.shared.lanes=0),Ia|=b,t.lanes=b,t.memoizedState=te}}function Zd(t,a){if(typeof t!="function")throw Error(i(191,t));t.call(a)}function Jd(t,a){var r=t.callbacks;if(r!==null)for(t.callbacks=null,t=0;th?h:8;var b=G.T,S={};G.T=S,to(t,!1,a,r);try{var v=f(),P=G.S;if(P!==null&&P(S,v),v!==null&&typeof v=="object"&&typeof v.then=="function"){var W=qb(v,l);Xr(t,a,W,rn(t))}else Xr(t,a,l,rn(t))}catch(te){Xr(t,a,{then:function(){},status:"rejected",reason:te},rn())}finally{re.p=h,b!==null&&S.types!==null&&(b.types=S.types),G.T=b}}function Zb(){}function $c(t,a,r,l){if(t.tag!==5)throw Error(i(476));var f=v0(t).queue;O0(t,f,a,Te,r===null?Zb:function(){return D0(t),r(l)})}function v0(t){var a=t.memoizedState;if(a!==null)return a;a={memoizedState:Te,baseState:Te,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ra,lastRenderedState:Te},next:null};var r={};return a.next={memoizedState:r,baseState:r,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ra,lastRenderedState:r},next:null},t.memoizedState=a,t=t.alternate,t!==null&&(t.memoizedState=a),a}function D0(t){var a=v0(t);a.next===null&&(a=t.alternate.memoizedState),Xr(t,a.next.queue,{},rn())}function eo(){return vt(fi)}function L0(){return ht().memoizedState}function I0(){return ht().memoizedState}function Jb(t){for(var a=t.return;a!==null;){switch(a.tag){case 24:case 3:var r=rn();t=xa(r);var l=Ra(a,t,r);l!==null&&(Zt(l,a,r),jr(l,a,r)),a={cache:vc()},t.payload=a;return}a=a.return}}function Wb(t,a,r){var l=rn();r={lane:l,revertLane:0,gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Ol(t)?M0(a,r):(r=Tc(t,a,r,l),r!==null&&(Zt(r,t,l),w0(r,a,l)))}function k0(t,a,r){var l=rn();Xr(t,a,r,l)}function Xr(t,a,r,l){var f={lane:l,revertLane:0,gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null};if(Ol(t))M0(a,f);else{var h=t.alternate;if(t.lanes===0&&(h===null||h.lanes===0)&&(h=a.lastRenderedReducer,h!==null))try{var b=a.lastRenderedState,S=h(b,r);if(f.hasEagerState=!0,f.eagerState=S,$t(S,b))return sl(t,a,f,0),$e===null&&ll(),!1}catch{}finally{}if(r=Tc(t,a,f,l),r!==null)return Zt(r,t,l),w0(r,a,l),!0}return!1}function to(t,a,r,l){if(l={lane:2,revertLane:Mo(),gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},Ol(t)){if(a)throw Error(i(479))}else a=Tc(t,r,l,2),a!==null&&Zt(a,t,2)}function Ol(t){var a=t.alternate;return t===De||a!==null&&a===De}function M0(t,a){Xu=_l=!0;var r=t.pending;r===null?a.next=a:(a.next=r.next,r.next=a),t.pending=a}function w0(t,a,r){if((r&4194048)!==0){var l=a.lanes;l&=t.pendingLanes,r|=l,a.lanes=r,Fh(t,r)}}var Kr={readContext:vt,use:Cl,useCallback:ct,useContext:ct,useEffect:ct,useImperativeHandle:ct,useLayoutEffect:ct,useInsertionEffect:ct,useMemo:ct,useReducer:ct,useRef:ct,useState:ct,useDebugValue:ct,useDeferredValue:ct,useTransition:ct,useSyncExternalStore:ct,useId:ct,useHostTransitionStatus:ct,useFormState:ct,useActionState:ct,useOptimistic:ct,useMemoCache:ct,useCacheRefresh:ct};Kr.useEffectEvent=ct;var B0={readContext:vt,use:Cl,useCallback:function(t,a){return Ht().memoizedState=[t,a===void 0?null:a],t},useContext:vt,useEffect:b0,useImperativeHandle:function(t,a,r){r=r!=null?r.concat([t]):null,xl(4194308,4,A0.bind(null,a,t),r)},useLayoutEffect:function(t,a){return xl(4194308,4,t,a)},useInsertionEffect:function(t,a){xl(4,2,t,a)},useMemo:function(t,a){var r=Ht();a=a===void 0?null:a;var l=t();if(hu){st(!0);try{t()}finally{st(!1)}}return r.memoizedState=[l,a],l},useReducer:function(t,a,r){var l=Ht();if(r!==void 0){var f=r(a);if(hu){st(!0);try{r(a)}finally{st(!1)}}}else f=a;return l.memoizedState=l.baseState=f,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:f},l.queue=t,t=t.dispatch=Wb.bind(null,De,t),[l.memoizedState,t]},useRef:function(t){var a=Ht();return t={current:t},a.memoizedState=t},useState:function(t){t=Xc(t);var a=t.queue,r=k0.bind(null,De,a);return a.dispatch=r,[t.memoizedState,r]},useDebugValue:Jc,useDeferredValue:function(t,a){var r=Ht();return Wc(r,t,a)},useTransition:function(){var t=Xc(!1);return t=O0.bind(null,De,t.queue,!0,!1),Ht().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,a,r){var l=De,f=Ht();if(ze){if(r===void 0)throw Error(i(407));r=r()}else{if(r=a(),$e===null)throw Error(i(349));(Pe&127)!==0||a0(l,a,r)}f.memoizedState=r;var h={value:r,getSnapshot:a};return f.queue=h,b0(r0.bind(null,l,h,t),[t]),l.flags|=2048,Zu(9,{destroy:void 0},u0.bind(null,l,h,r,a),null),r},useId:function(){var t=Ht(),a=$e.identifierPrefix;if(ze){var r=Hn,l=Un;r=(l&~(1<<32-mt(l)-1)).toString(32)+r,a="_"+a+"R_"+r,r=Sl++,0<\/script>",h=h.removeChild(h.firstChild);break;case"select":h=typeof l.is=="string"?b.createElement("select",{is:l.is}):b.createElement("select"),l.multiple?h.multiple=!0:l.size&&(h.size=l.size);break;default:h=typeof l.is=="string"?b.createElement(f,{is:l.is}):b.createElement(f)}}h[Rt]=a,h[qt]=l;e:for(b=a.child;b!==null;){if(b.tag===5||b.tag===6)h.appendChild(b.stateNode);else if(b.tag!==4&&b.tag!==27&&b.child!==null){b.child.return=b,b=b.child;continue}if(b===a)break e;for(;b.sibling===null;){if(b.return===null||b.return===a)break e;b=b.return}b.sibling.return=b.return,b=b.sibling}a.stateNode=h;e:switch(Lt(h,f,l),f){case"button":case"input":case"select":case"textarea":l=!!l.autoFocus;break e;case"img":l=!0;break e;default:l=!1}l&&la(a)}}return ut(a),Eo(a,a.type,t===null?null:t.memoizedProps,a.pendingProps,r),null;case 6:if(t&&a.stateNode!=null)t.memoizedProps!==l&&la(a);else{if(typeof l!="string"&&a.stateNode===null)throw Error(i(166));if(t=se.current,zu(a)){if(t=a.stateNode,r=a.memoizedProps,l=null,f=Ot,f!==null)switch(f.tag){case 27:case 5:l=f.memoizedProps}t[Rt]=a,t=!!(t.nodeValue===r||l!==null&&l.suppressHydrationWarning===!0||tp(t.nodeValue,r)),t||Aa(a,!0)}else t=Xl(t).createTextNode(l),t[Rt]=a,a.stateNode=t}return ut(a),null;case 31:if(r=a.memoizedState,t===null||t.memoizedState!==null){if(l=zu(a),r!==null){if(t===null){if(!l)throw Error(i(318));if(t=a.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(i(557));t[Rt]=a}else ru(),(a.flags&128)===0&&(a.memoizedState=null),a.flags|=4;ut(a),t=!1}else r=Nc(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=r),t=!0;if(!t)return a.flags&256?(nn(a),a):(nn(a),null);if((a.flags&128)!==0)throw Error(i(558))}return ut(a),null;case 13:if(l=a.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(f=zu(a),l!==null&&l.dehydrated!==null){if(t===null){if(!f)throw Error(i(318));if(f=a.memoizedState,f=f!==null?f.dehydrated:null,!f)throw Error(i(317));f[Rt]=a}else ru(),(a.flags&128)===0&&(a.memoizedState=null),a.flags|=4;ut(a),f=!1}else f=Nc(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=f),f=!0;if(!f)return a.flags&256?(nn(a),a):(nn(a),null)}return nn(a),(a.flags&128)!==0?(a.lanes=r,a):(r=l!==null,t=t!==null&&t.memoizedState!==null,r&&(l=a.child,f=null,l.alternate!==null&&l.alternate.memoizedState!==null&&l.alternate.memoizedState.cachePool!==null&&(f=l.alternate.memoizedState.cachePool.pool),h=null,l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(h=l.memoizedState.cachePool.pool),h!==f&&(l.flags|=2048)),r!==t&&r&&(a.child.flags|=8192),kl(a,a.updateQueue),ut(a),null);case 4:return ve(),t===null&&Ho(a.stateNode.containerInfo),ut(a),null;case 10:return aa(a.type),ut(a),null;case 19:if(Z(ft),l=a.memoizedState,l===null)return ut(a),null;if(f=(a.flags&128)!==0,h=l.rendering,h===null)if(f)Jr(l,!1);else{if(ot!==0||t!==null&&(t.flags&128)!==0)for(t=a.child;t!==null;){if(h=yl(t),h!==null){for(a.flags|=128,Jr(l,!1),t=h.updateQueue,a.updateQueue=t,kl(a,t),a.subtreeFlags=0,t=r,r=a.child;r!==null;)kd(r,t),r=r.sibling;return C(ft,ft.current&1|2),ze&&ta(a,l.treeForkCount),a.child}t=t.sibling}l.tail!==null&&Ut()>Hl&&(a.flags|=128,f=!0,Jr(l,!1),a.lanes=4194304)}else{if(!f)if(t=yl(h),t!==null){if(a.flags|=128,f=!0,t=t.updateQueue,a.updateQueue=t,kl(a,t),Jr(l,!0),l.tail===null&&l.tailMode==="hidden"&&!h.alternate&&!ze)return ut(a),null}else 2*Ut()-l.renderingStartTime>Hl&&r!==536870912&&(a.flags|=128,f=!0,Jr(l,!1),a.lanes=4194304);l.isBackwards?(h.sibling=a.child,a.child=h):(t=l.last,t!==null?t.sibling=h:a.child=h,l.last=h)}return l.tail!==null?(t=l.tail,l.rendering=t,l.tail=t.sibling,l.renderingStartTime=Ut(),t.sibling=null,r=ft.current,C(ft,f?r&1|2:r&1),ze&&ta(a,l.treeForkCount),t):(ut(a),null);case 22:case 23:return nn(a),Hc(),l=a.memoizedState!==null,t!==null?t.memoizedState!==null!==l&&(a.flags|=8192):l&&(a.flags|=8192),l?(r&536870912)!==0&&(a.flags&128)===0&&(ut(a),a.subtreeFlags&6&&(a.flags|=8192)):ut(a),r=a.updateQueue,r!==null&&kl(a,r.retryQueue),r=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),l=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(l=a.memoizedState.cachePool.pool),l!==r&&(a.flags|=2048),t!==null&&Z(su),null;case 24:return r=null,t!==null&&(r=t.memoizedState.cache),a.memoizedState.cache!==r&&(a.flags|=2048),aa(pt),ut(a),null;case 25:return null;case 30:return null}throw Error(i(156,a.tag))}function a2(t,a){switch(Ac(a),a.tag){case 1:return t=a.flags,t&65536?(a.flags=t&-65537|128,a):null;case 3:return aa(pt),ve(),t=a.flags,(t&65536)!==0&&(t&128)===0?(a.flags=t&-65537|128,a):null;case 26:case 27:case 5:return We(a),null;case 31:if(a.memoizedState!==null){if(nn(a),a.alternate===null)throw Error(i(340));ru()}return t=a.flags,t&65536?(a.flags=t&-65537|128,a):null;case 13:if(nn(a),t=a.memoizedState,t!==null&&t.dehydrated!==null){if(a.alternate===null)throw Error(i(340));ru()}return t=a.flags,t&65536?(a.flags=t&-65537|128,a):null;case 19:return Z(ft),null;case 4:return ve(),null;case 10:return aa(a.type),null;case 22:case 23:return nn(a),Hc(),t!==null&&Z(su),t=a.flags,t&65536?(a.flags=t&-65537|128,a):null;case 24:return aa(pt),null;case 25:return null;default:return null}}function im(t,a){switch(Ac(a),a.tag){case 3:aa(pt),ve();break;case 26:case 27:case 5:We(a);break;case 4:ve();break;case 31:a.memoizedState!==null&&nn(a);break;case 13:nn(a);break;case 19:Z(ft);break;case 10:aa(a.type);break;case 22:case 23:nn(a),Hc(),t!==null&&Z(su);break;case 24:aa(pt)}}function Wr(t,a){try{var r=a.updateQueue,l=r!==null?r.lastEffect:null;if(l!==null){var f=l.next;r=f;do{if((r.tag&t)===t){l=void 0;var h=r.create,b=r.inst;l=h(),b.destroy=l}r=r.next}while(r!==f)}}catch(S){Xe(a,a.return,S)}}function Da(t,a,r){try{var l=a.updateQueue,f=l!==null?l.lastEffect:null;if(f!==null){var h=f.next;l=h;do{if((l.tag&t)===t){var b=l.inst,S=b.destroy;if(S!==void 0){b.destroy=void 0,f=a;var v=r,P=S;try{P()}catch(W){Xe(f,v,W)}}}l=l.next}while(l!==h)}}catch(W){Xe(a,a.return,W)}}function lm(t){var a=t.updateQueue;if(a!==null){var r=t.stateNode;try{Jd(a,r)}catch(l){Xe(t,t.return,l)}}}function sm(t,a,r){r.props=du(t.type,t.memoizedProps),r.state=t.memoizedState;try{r.componentWillUnmount()}catch(l){Xe(t,a,l)}}function $r(t,a){try{var r=t.ref;if(r!==null){switch(t.tag){case 26:case 27:case 5:var l=t.stateNode;break;case 30:l=t.stateNode;break;default:l=t.stateNode}typeof r=="function"?t.refCleanup=r(l):r.current=l}}catch(f){Xe(t,a,f)}}function Pn(t,a){var r=t.ref,l=t.refCleanup;if(r!==null)if(typeof l=="function")try{l()}catch(f){Xe(t,a,f)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof r=="function")try{r(null)}catch(f){Xe(t,a,f)}else r.current=null}function cm(t){var a=t.type,r=t.memoizedProps,l=t.stateNode;try{e:switch(a){case"button":case"input":case"select":case"textarea":r.autoFocus&&l.focus();break e;case"img":r.src?l.src=r.src:r.srcSet&&(l.srcset=r.srcSet)}}catch(f){Xe(t,t.return,f)}}function go(t,a,r){try{var l=t.stateNode;N2(l,t.type,r,a),l[qt]=a}catch(f){Xe(t,t.return,f)}}function om(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&Ua(t.type)||t.tag===4}function To(t){e:for(;;){for(;t.sibling===null;){if(t.return===null||om(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&Ua(t.type)||t.flags&2||t.child===null||t.tag===4)continue e;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function bo(t,a,r){var l=t.tag;if(l===5||l===6)t=t.stateNode,a?(r.nodeType===9?r.body:r.nodeName==="HTML"?r.ownerDocument.body:r).insertBefore(t,a):(a=r.nodeType===9?r.body:r.nodeName==="HTML"?r.ownerDocument.body:r,a.appendChild(t),r=r._reactRootContainer,r!=null||a.onclick!==null||(a.onclick=Wn));else if(l!==4&&(l===27&&Ua(t.type)&&(r=t.stateNode,a=null),t=t.child,t!==null))for(bo(t,a,r),t=t.sibling;t!==null;)bo(t,a,r),t=t.sibling}function Ml(t,a,r){var l=t.tag;if(l===5||l===6)t=t.stateNode,a?r.insertBefore(t,a):r.appendChild(t);else if(l!==4&&(l===27&&Ua(t.type)&&(r=t.stateNode),t=t.child,t!==null))for(Ml(t,a,r),t=t.sibling;t!==null;)Ml(t,a,r),t=t.sibling}function fm(t){var a=t.stateNode,r=t.memoizedProps;try{for(var l=t.type,f=a.attributes;f.length;)a.removeAttributeNode(f[0]);Lt(a,l,r),a[Rt]=t,a[qt]=r}catch(h){Xe(t,t.return,h)}}var sa=!1,Tt=!1,yo=!1,hm=typeof WeakSet=="function"?WeakSet:Set,Ct=null;function u2(t,a){if(t=t.containerInfo,zo=ts,t=Cd(t),hc(t)){if("selectionStart"in t)var r={start:t.selectionStart,end:t.selectionEnd};else e:{r=(r=t.ownerDocument)&&r.defaultView||window;var l=r.getSelection&&r.getSelection();if(l&&l.rangeCount!==0){r=l.anchorNode;var f=l.anchorOffset,h=l.focusNode;l=l.focusOffset;try{r.nodeType,h.nodeType}catch{r=null;break e}var b=0,S=-1,v=-1,P=0,W=0,te=t,z=null;t:for(;;){for(var V;te!==r||f!==0&&te.nodeType!==3||(S=b+f),te!==h||l!==0&&te.nodeType!==3||(v=b+l),te.nodeType===3&&(b+=te.nodeValue.length),(V=te.firstChild)!==null;)z=te,te=V;for(;;){if(te===t)break t;if(z===r&&++P===f&&(S=b),z===h&&++W===l&&(v=b),(V=te.nextSibling)!==null)break;te=z,z=te.parentNode}te=V}r=S===-1||v===-1?null:{start:S,end:v}}else r=null}r=r||{start:0,end:0}}else r=null;for(Yo={focusedElem:t,selectionRange:r},ts=!1,Ct=a;Ct!==null;)if(a=Ct,t=a.child,(a.subtreeFlags&1028)!==0&&t!==null)t.return=a,Ct=t;else for(;Ct!==null;){switch(a=Ct,h=a.alternate,t=a.flags,a.tag){case 0:if((t&4)!==0&&(t=a.updateQueue,t=t!==null?t.events:null,t!==null))for(r=0;r title"))),Lt(h,l,r),h[Rt]=t,At(h),l=h;break e;case"link":var b=Tp("link","href",f).get(l+(r.href||""));if(b){for(var S=0;SJe&&(b=Je,Je=Ne,Ne=b);var B=Sd(S,Ne),k=Sd(S,Je);if(B&&k&&(V.rangeCount!==1||V.anchorNode!==B.node||V.anchorOffset!==B.offset||V.focusNode!==k.node||V.focusOffset!==k.offset)){var H=te.createRange();H.setStart(B.node,B.offset),V.removeAllRanges(),Ne>Je?(V.addRange(H),V.extend(k.node,k.offset)):(H.setEnd(k.node,k.offset),V.addRange(H))}}}}for(te=[],V=S;V=V.parentNode;)V.nodeType===1&&te.push({element:V,left:V.scrollLeft,top:V.scrollTop});for(typeof S.focus=="function"&&S.focus(),S=0;Sr?32:r,G.T=null,r=Ro,Ro=null;var h=Ma,b=da;if(_t=0,tr=Ma=null,da=0,(Ge&6)!==0)throw Error(i(331));var S=Ge;if(Ge|=4,Am(h.current),ym(h,h.current,b,r),Ge=S,ri(0,!1),yt&&typeof yt.onPostCommitFiberRoot=="function")try{yt.onPostCommitFiberRoot(xn,h)}catch{}return!0}finally{re.p=f,G.T=l,zm(t,a)}}function jm(t,a,r){a=fn(r,a),a=ro(t.stateNode,a,2),t=Ra(t,a,2),t!==null&&(Nr(t,2),Fn(t))}function Xe(t,a,r){if(t.tag===3)jm(t,t,r);else for(;a!==null;){if(a.tag===3){jm(a,t,r);break}else if(a.tag===1){var l=a.stateNode;if(typeof a.type.getDerivedStateFromError=="function"||typeof l.componentDidCatch=="function"&&(ka===null||!ka.has(l))){t=fn(r,t),r=q0(2),l=Ra(a,r,2),l!==null&&(G0(r,l,a,t),Nr(l,2),Fn(l));break}}a=a.return}}function Lo(t,a,r){var l=t.pingCache;if(l===null){l=t.pingCache=new l2;var f=new Set;l.set(a,f)}else f=l.get(a),f===void 0&&(f=new Set,l.set(a,f));f.has(r)||(Ao=!0,f.add(r),t=h2.bind(null,t,a,r),a.then(t,t))}function h2(t,a,r){var l=t.pingCache;l!==null&&l.delete(a),t.pingedLanes|=t.suspendedLanes&r,t.warmLanes&=~r,$e===t&&(Pe&r)===r&&(ot===4||ot===3&&(Pe&62914560)===Pe&&300>Ut()-Ul?(Ge&2)===0&&nr(t,0):Co|=r,er===Pe&&(er=0)),Fn(t)}function qm(t,a){a===0&&(a=Hh()),t=au(t,a),t!==null&&(Nr(t,a),Fn(t))}function d2(t){var a=t.memoizedState,r=0;a!==null&&(r=a.retryLane),qm(t,r)}function m2(t,a){var r=0;switch(t.tag){case 31:case 13:var l=t.stateNode,f=t.memoizedState;f!==null&&(r=f.retryLane);break;case 19:l=t.stateNode;break;case 22:l=t.stateNode._retryCache;break;default:throw Error(i(314))}l!==null&&l.delete(a),qm(t,r)}function p2(t,a){return Bn(t,a)}var ql=null,ur=null,Io=!1,Gl=!1,ko=!1,Ba=0;function Fn(t){t!==ur&&t.next===null&&(ur===null?ql=ur=t:ur=ur.next=t),Gl=!0,Io||(Io=!0,g2())}function ri(t,a){if(!ko&&Gl){ko=!0;do for(var r=!1,l=ql;l!==null;){if(t!==0){var f=l.pendingLanes;if(f===0)var h=0;else{var b=l.suspendedLanes,S=l.pingedLanes;h=(1<<31-mt(42|t)+1)-1,h&=f&~(b&~S),h=h&201326741?h&201326741|1:h?h|2:0}h!==0&&(r=!0,Xm(l,h))}else h=Pe,h=Ki(l,l===$e?h:0,l.cancelPendingCommit!==null||l.timeoutHandle!==-1),(h&3)===0||Cr(l,h)||(r=!0,Xm(l,h));l=l.next}while(r);ko=!1}}function E2(){Gm()}function Gm(){Gl=Io=!1;var t=0;Ba!==0&&R2()&&(t=Ba);for(var a=Ut(),r=null,l=ql;l!==null;){var f=l.next,h=Qm(l,a);h===0?(l.next=null,r===null?ql=f:r.next=f,f===null&&(ur=r)):(r=l,(t!==0||(h&3)!==0)&&(Gl=!0)),l=f}_t!==0&&_t!==5||ri(t),Ba!==0&&(Ba=0)}function Qm(t,a){for(var r=t.suspendedLanes,l=t.pingedLanes,f=t.expirationTimes,h=t.pendingLanes&-62914561;0S)break;var W=v.transferSize,te=v.initiatorType;W&&np(te)&&(v=v.responseEnd,b+=W*(v"u"?null:document;function mp(t,a,r){var l=rr;if(l&&typeof a=="string"&&a){var f=cn(a);f='link[rel="'+t+'"][href="'+f+'"]',typeof r=="string"&&(f+='[crossorigin="'+r+'"]'),dp.has(f)||(dp.add(f),t={rel:t,crossOrigin:r,href:a},l.querySelector(f)===null&&(a=l.createElement("link"),Lt(a,"link",t),At(a),l.head.appendChild(a)))}}function B2(t){ma.D(t),mp("dns-prefetch",t,null)}function U2(t,a){ma.C(t,a),mp("preconnect",t,a)}function H2(t,a,r){ma.L(t,a,r);var l=rr;if(l&&t&&a){var f='link[rel="preload"][as="'+cn(a)+'"]';a==="image"&&r&&r.imageSrcSet?(f+='[imagesrcset="'+cn(r.imageSrcSet)+'"]',typeof r.imageSizes=="string"&&(f+='[imagesizes="'+cn(r.imageSizes)+'"]')):f+='[href="'+cn(t)+'"]';var h=f;switch(a){case"style":h=ir(t);break;case"script":h=lr(t)}gn.has(h)||(t=T({rel:"preload",href:a==="image"&&r&&r.imageSrcSet?void 0:t,as:a},r),gn.set(h,t),l.querySelector(f)!==null||a==="style"&&l.querySelector(ci(h))||a==="script"&&l.querySelector(oi(h))||(a=l.createElement("link"),Lt(a,"link",t),At(a),l.head.appendChild(a)))}}function P2(t,a){ma.m(t,a);var r=rr;if(r&&t){var l=a&&typeof a.as=="string"?a.as:"script",f='link[rel="modulepreload"][as="'+cn(l)+'"][href="'+cn(t)+'"]',h=f;switch(l){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":h=lr(t)}if(!gn.has(h)&&(t=T({rel:"modulepreload",href:t},a),gn.set(h,t),r.querySelector(f)===null)){switch(l){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(r.querySelector(oi(h)))return}l=r.createElement("link"),Lt(l,"link",t),At(l),r.head.appendChild(l)}}}function F2(t,a,r){ma.S(t,a,r);var l=rr;if(l&&t){var f=Ou(l).hoistableStyles,h=ir(t);a=a||"default";var b=f.get(h);if(!b){var S={loading:0,preload:null};if(b=l.querySelector(ci(h)))S.loading=5;else{t=T({rel:"stylesheet",href:t,"data-precedence":a},r),(r=gn.get(h))&&Ko(t,r);var v=b=l.createElement("link");At(v),Lt(v,"link",t),v._p=new Promise(function(P,W){v.onload=P,v.onerror=W}),v.addEventListener("load",function(){S.loading|=1}),v.addEventListener("error",function(){S.loading|=2}),S.loading|=4,Zl(b,a,l)}b={type:"stylesheet",instance:b,count:1,state:S},f.set(h,b)}}}function z2(t,a){ma.X(t,a);var r=rr;if(r&&t){var l=Ou(r).hoistableScripts,f=lr(t),h=l.get(f);h||(h=r.querySelector(oi(f)),h||(t=T({src:t,async:!0},a),(a=gn.get(f))&&Zo(t,a),h=r.createElement("script"),At(h),Lt(h,"link",t),r.head.appendChild(h)),h={type:"script",instance:h,count:1,state:null},l.set(f,h))}}function Y2(t,a){ma.M(t,a);var r=rr;if(r&&t){var l=Ou(r).hoistableScripts,f=lr(t),h=l.get(f);h||(h=r.querySelector(oi(f)),h||(t=T({src:t,async:!0,type:"module"},a),(a=gn.get(f))&&Zo(t,a),h=r.createElement("script"),At(h),Lt(h,"link",t),r.head.appendChild(h)),h={type:"script",instance:h,count:1,state:null},l.set(f,h))}}function pp(t,a,r,l){var f=(f=se.current)?Kl(f):null;if(!f)throw Error(i(446));switch(t){case"meta":case"title":return null;case"style":return typeof r.precedence=="string"&&typeof r.href=="string"?(a=ir(r.href),r=Ou(f).hoistableStyles,l=r.get(a),l||(l={type:"style",instance:null,count:0,state:null},r.set(a,l)),l):{type:"void",instance:null,count:0,state:null};case"link":if(r.rel==="stylesheet"&&typeof r.href=="string"&&typeof r.precedence=="string"){t=ir(r.href);var h=Ou(f).hoistableStyles,b=h.get(t);if(b||(f=f.ownerDocument||f,b={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},h.set(t,b),(h=f.querySelector(ci(t)))&&!h._p&&(b.instance=h,b.state.loading=5),gn.has(t)||(r={rel:"preload",as:"style",href:r.href,crossOrigin:r.crossOrigin,integrity:r.integrity,media:r.media,hrefLang:r.hrefLang,referrerPolicy:r.referrerPolicy},gn.set(t,r),h||j2(f,t,r,b.state))),a&&l===null)throw Error(i(528,""));return b}if(a&&l!==null)throw Error(i(529,""));return null;case"script":return a=r.async,r=r.src,typeof r=="string"&&a&&typeof a!="function"&&typeof a!="symbol"?(a=lr(r),r=Ou(f).hoistableScripts,l=r.get(a),l||(l={type:"script",instance:null,count:0,state:null},r.set(a,l)),l):{type:"void",instance:null,count:0,state:null};default:throw Error(i(444,t))}}function ir(t){return'href="'+cn(t)+'"'}function ci(t){return'link[rel="stylesheet"]['+t+"]"}function Ep(t){return T({},t,{"data-precedence":t.precedence,precedence:null})}function j2(t,a,r,l){t.querySelector('link[rel="preload"][as="style"]['+a+"]")?l.loading=1:(a=t.createElement("link"),l.preload=a,a.addEventListener("load",function(){return l.loading|=1}),a.addEventListener("error",function(){return l.loading|=2}),Lt(a,"link",r),At(a),t.head.appendChild(a))}function lr(t){return'[src="'+cn(t)+'"]'}function oi(t){return"script[async]"+t}function gp(t,a,r){if(a.count++,a.instance===null)switch(a.type){case"style":var l=t.querySelector('style[data-href~="'+cn(r.href)+'"]');if(l)return a.instance=l,At(l),l;var f=T({},r,{"data-href":r.href,"data-precedence":r.precedence,href:null,precedence:null});return l=(t.ownerDocument||t).createElement("style"),At(l),Lt(l,"style",f),Zl(l,r.precedence,t),a.instance=l;case"stylesheet":f=ir(r.href);var h=t.querySelector(ci(f));if(h)return a.state.loading|=4,a.instance=h,At(h),h;l=Ep(r),(f=gn.get(f))&&Ko(l,f),h=(t.ownerDocument||t).createElement("link"),At(h);var b=h;return b._p=new Promise(function(S,v){b.onload=S,b.onerror=v}),Lt(h,"link",l),a.state.loading|=4,Zl(h,r.precedence,t),a.instance=h;case"script":return h=lr(r.src),(f=t.querySelector(oi(h)))?(a.instance=f,At(f),f):(l=r,(f=gn.get(h))&&(l=T({},r),Zo(l,f)),t=t.ownerDocument||t,f=t.createElement("script"),At(f),Lt(f,"link",l),t.head.appendChild(f),a.instance=f);case"void":return null;default:throw Error(i(443,a.type))}else a.type==="stylesheet"&&(a.state.loading&4)===0&&(l=a.instance,a.state.loading|=4,Zl(l,r.precedence,t));return a.instance}function Zl(t,a,r){for(var l=r.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),f=l.length?l[l.length-1]:null,h=f,b=0;b title"):null)}function q2(t,a,r){if(r===1||a.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof a.precedence!="string"||typeof a.href!="string"||a.href==="")break;return!0;case"link":if(typeof a.rel!="string"||typeof a.href!="string"||a.href===""||a.onLoad||a.onError)break;switch(a.rel){case"stylesheet":return t=a.disabled,typeof a.precedence=="string"&&t==null;default:return!0}case"script":if(a.async&&typeof a.async!="function"&&typeof a.async!="symbol"&&!a.onLoad&&!a.onError&&a.src&&typeof a.src=="string")return!0}return!1}function yp(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function G2(t,a,r,l){if(r.type==="stylesheet"&&(typeof l.media!="string"||matchMedia(l.media).matches!==!1)&&(r.state.loading&4)===0){if(r.instance===null){var f=ir(l.href),h=a.querySelector(ci(f));if(h){a=h._p,a!==null&&typeof a=="object"&&typeof a.then=="function"&&(t.count++,t=Wl.bind(t),a.then(t,t)),r.state.loading|=4,r.instance=h,At(h);return}h=a.ownerDocument||a,l=Ep(l),(f=gn.get(f))&&Ko(l,f),h=h.createElement("link"),At(h);var b=h;b._p=new Promise(function(S,v){b.onload=S,b.onerror=v}),Lt(h,"link",l),r.instance=h}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(r,a),(a=r.state.preload)&&(r.state.loading&3)===0&&(t.count++,r=Wl.bind(t),a.addEventListener("load",r),a.addEventListener("error",r))}}var Jo=0;function Q2(t,a){return t.stylesheets&&t.count===0&&es(t,t.stylesheets),0Jo?50:800)+a);return t.unsuspend=r,function(){t.unsuspend=null,clearTimeout(l),clearTimeout(f)}}:null}function Wl(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)es(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var $l=null;function es(t,a){t.stylesheets=null,t.unsuspend!==null&&(t.count++,$l=new Map,a.forEach(V2,t),$l=null,Wl.call(t))}function V2(t,a){if(!(a.state.loading&4)){var r=$l.get(t);if(r)var l=r.get(null);else{r=new Map,$l.set(t,r);for(var f=t.querySelectorAll("link[data-precedence],style[data-precedence]"),h=0;h"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(n){console.error(n)}}return e(),sf.exports=fy(),sf.exports}var dy=hy();/** - * react-router v7.18.1 - * - * Copyright (c) Remix Software Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE.md file in the root directory of this source tree. - * - * @license MIT - */var lh=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,gE=/^[\\/]{2}/;function my(e,n){return n+e.replace(/\\/g,"/")}var qp="popstate";function Gp(e){return typeof e=="object"&&e!=null&&"pathname"in e&&"search"in e&&"hash"in e&&"state"in e&&"key"in e}function py(e={}){function n(i,s){var p;let c=(p=s.state)==null?void 0:p.masked,{pathname:o,search:d,hash:E}=c||i.location;return Pf("",{pathname:o,search:d,hash:E},s.state&&s.state.usr||null,s.state&&s.state.key||"default",c?{pathname:i.location.pathname,search:i.location.search,hash:i.location.hash}:void 0)}function u(i,s){return typeof s=="string"?s:Li(s)}return gy(n,u,null,e)}function lt(e,n){if(e===!1||e===null||typeof e>"u")throw new Error(n)}function Mn(e,n){if(!e){typeof console<"u"&&console.warn(n);try{throw new Error(n)}catch{}}}function Ey(){return Math.random().toString(36).substring(2,10)}function Qp(e,n){return{usr:e.state,key:e.key,idx:n,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function Pf(e,n,u=null,i,s){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof n=="string"?gr(n):n,state:u,key:n&&n.key||i||Ey(),mask:s}}function Li({pathname:e="/",search:n="",hash:u=""}){return n&&n!=="?"&&(e+=n.charAt(0)==="?"?n:"?"+n),u&&u!=="#"&&(e+=u.charAt(0)==="#"?u:"#"+u),e}function gr(e){let n={};if(e){let u=e.indexOf("#");u>=0&&(n.hash=e.substring(u),e=e.substring(0,u));let i=e.indexOf("?");i>=0&&(n.search=e.substring(i),e=e.substring(0,i)),e&&(n.pathname=e)}return n}function gy(e,n,u,i={}){let{window:s=document.defaultView,v5Compat:c=!1}=i,o=s.history,d="POP",E=null,p=g();p==null&&(p=0,o.replaceState({...o.state,idx:p},""));function g(){return(o.state||{idx:null}).idx}function T(){d="POP";let w=g(),L=w==null?null:w-p;p=w,E&&E({action:d,location:I.location,delta:L})}function _(w,L){d="PUSH";let X=Gp(w)?w:Pf(I.location,w,L);p=g()+1;let q=Qp(X,p),he=I.createHref(X.mask||X);try{o.pushState(q,"",he)}catch(de){if(de instanceof DOMException&&de.name==="DataCloneError")throw de;s.location.assign(he)}c&&E&&E({action:d,location:I.location,delta:1})}function y(w,L){d="REPLACE";let X=Gp(w)?w:Pf(I.location,w,L);p=g();let q=Qp(X,p),he=I.createHref(X.mask||X);o.replaceState(q,"",he),c&&E&&E({action:d,location:I.location,delta:0})}function R(w){return Ty(s,w)}let I={get action(){return d},get location(){return e(s,o)},listen(w){if(E)throw new Error("A history only accepts one active listener");return s.addEventListener(qp,T),E=w,()=>{s.removeEventListener(qp,T),E=null}},createHref(w){return n(s,w)},createURL:R,encodeLocation(w){let L=R(w);return{pathname:L.pathname,search:L.search,hash:L.hash}},push:_,replace:y,go(w){return o.go(w)}};return I}function Ty(e,n,u=!1){let i="http://localhost";e&&(i=e.location.origin!=="null"?e.location.origin:e.location.href),lt(i,"No window.location.(origin|href) available to create URL");let s=typeof n=="string"?n:Li(n);return s=s.replace(/ $/,"%20"),!u&&gE.test(s)&&(s=i+s),new URL(s,i)}function TE(e,n,u="/"){return by(e,n,u,!1)}function by(e,n,u,i,s){let c=typeof n=="string"?gr(n):n,o=Ea(c.pathname||"/",u);if(o==null)return null;let d=yy(e),E=null,p=Ly(o);for(let g=0;E==null&&g{let g={relativePath:p===void 0?o.path||"":p,caseSensitive:o.caseSensitive===!0,childrenIndex:d,route:o};if(g.relativePath.startsWith("/")){if(!g.relativePath.startsWith(i)&&E)return;lt(g.relativePath.startsWith(i),`Absolute route path "${g.relativePath}" nested under path "${i}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),g.relativePath=g.relativePath.slice(i.length)}let T=In([i,g.relativePath]),_=u.concat(g);o.children&&o.children.length>0&&(lt(o.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${T}".`),bE(o.children,n,_,T,E)),!(o.path==null&&!o.index)&&n.push({path:T,score:Oy(T,o.index),routesMeta:_.map((y,R)=>{let[I,w]=SE(y.relativePath,y.caseSensitive,R===_.length-1);return{...y,matcher:I,compiledParams:w}})})};return e.forEach((o,d)=>{var E;if(o.path===""||!((E=o.path)!=null&&E.includes("?")))c(o,d);else for(let p of yE(o.path))c(o,d,!0,p)}),n}function yE(e){let n=e.split("/");if(n.length===0)return[];let[u,...i]=n,s=u.endsWith("?"),c=u.replace(/\?$/,"");if(i.length===0)return s?[c,""]:[c];let o=yE(i.join("/")),d=[];return d.push(...o.map(E=>E===""?c:[c,E].join("/"))),s&&d.push(...o),d.map(E=>e.startsWith("/")&&E===""?"/":E)}function _y(e){e.sort((n,u)=>n.score!==u.score?u.score-n.score:vy(n.routesMeta.map(i=>i.childrenIndex),u.routesMeta.map(i=>i.childrenIndex)))}var Sy=/^:[\w-]+$/,Ay=3,Cy=2,Ny=1,xy=10,Ry=-2,Vp=e=>e==="*";function Oy(e,n){let u=e.split("/"),i=u.length;return u.some(Vp)&&(i+=Ry),n&&(i+=Cy),u.filter(s=>!Vp(s)).reduce((s,c)=>s+(Sy.test(c)?Ay:c===""?Ny:xy),i)}function vy(e,n){return e.length===n.length&&e.slice(0,-1).every((i,s)=>i===n[s])?e[e.length-1]-n[n.length-1]:0}function Dy(e,n,u=!1){let{routesMeta:i}=e,s={},c="/",o=[];for(let d=0;d{if(g==="*"){let R=d[_]||"";o=c.slice(0,c.length-R.length).replace(/(.)\/+$/,"$1")}const y=d[_];return T&&!y?p[g]=void 0:p[g]=(y||"").replace(/%2F/g,"/"),p},{}),pathname:c,pathnameBase:o,pattern:e}}function SE(e,n=!1,u=!0){Mn(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let i=[],s="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(o,d,E,p,g)=>{if(i.push({paramName:d,isOptional:E!=null}),E){let T=g.charAt(p+o.length);return T&&T!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return e.endsWith("*")?(i.push({paramName:"*"}),s+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):u?s+="\\/*$":e!==""&&e!=="/"&&(s+="(?:(?=\\/|$))"),[new RegExp(s,n?void 0:"i"),i]}function Ly(e){try{return e.split("/").map(n=>decodeURIComponent(n).replace(/\//g,"%2F")).join("/")}catch(n){return Mn(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${n}).`),e}}function Ea(e,n){if(n==="/")return e;if(!e.toLowerCase().startsWith(n.toLowerCase()))return null;let u=n.endsWith("/")?n.length-1:n.length,i=e.charAt(u);return i&&i!=="/"?null:e.slice(u)||"/"}function Iy(e,n="/"){let{pathname:u,search:i="",hash:s=""}=typeof e=="string"?gr(e):e,c;return u?(u=CE(u),u.startsWith("/")?c=Xp(u.substring(1),"/"):c=Xp(u,n)):c=n,{pathname:c,search:wy(i),hash:By(s)}}function Xp(e,n){let u=ys(n).split("/");return e.split("/").forEach(s=>{s===".."?u.length>1&&u.pop():s!=="."&&u.push(s)}),u.length>1?u.join("/"):"/"}function hf(e,n,u,i){return`Cannot include a '${e}' character in a manually specified \`to.${n}\` field [${JSON.stringify(i)}]. Please separate it out to the \`to.${u}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function ky(e){return e.filter((n,u)=>u===0||n.route.path&&n.route.path.length>0)}function AE(e){let n=ky(e);return n.map((u,i)=>i===n.length-1?u.pathname:u.pathnameBase)}function sh(e,n,u,i=!1){let s;typeof e=="string"?s=gr(e):(s={...e},lt(!s.pathname||!s.pathname.includes("?"),hf("?","pathname","search",s)),lt(!s.pathname||!s.pathname.includes("#"),hf("#","pathname","hash",s)),lt(!s.search||!s.search.includes("#"),hf("#","search","hash",s)));let c=e===""||s.pathname==="",o=c?"/":s.pathname,d;if(o==null)d=u;else{let T=n.length-1;if(!i&&o.startsWith("..")){let _=o.split("/");for(;_[0]==="..";)_.shift(),T-=1;s.pathname=_.join("/")}d=T>=0?n[T]:"/"}let E=Iy(s,d),p=o&&o!=="/"&&o.endsWith("/"),g=(c||o===".")&&u.endsWith("/");return!E.pathname.endsWith("/")&&(p||g)&&(E.pathname+="/"),E}var CE=e=>e.replace(/[\\/]{2,}/g,"/"),In=e=>CE(e.join("/")),ys=e=>e.replace(/\/+$/,""),My=e=>ys(e).replace(/^\/*/,"/"),wy=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,By=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e,Uy=class{constructor(e,n,u,i=!1){this.status=e,this.statusText=n||"",this.internal=i,u instanceof Error?(this.data=u.toString(),this.error=u):this.data=u}};function Hy(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}function Py(e){let n=e.map(u=>u.route.path).filter(Boolean);return In(n)||"/"}var NE=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function xE(e,n){let u=e;if(typeof u!="string"||!lh.test(u))return{absoluteURL:void 0,isExternal:!1,to:u};let i=u,s=!1;if(NE)try{let c=new URL(window.location.href),o=gE.test(u)?new URL(my(u,c.protocol)):new URL(u),d=Ea(o.pathname,n);o.origin===c.origin&&d!=null?u=d+o.search+o.hash:s=!0}catch{Mn(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:i,isExternal:s,to:u}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var RE=["POST","PUT","PATCH","DELETE"];new Set(RE);var Fy=["GET",...RE];new Set(Fy);var zy=["about:","blob:","chrome:","chrome-untrusted:","content:","data:","devtools:","file:","filesystem:","javascript:"];function Yy(e){try{return zy.includes(new URL(e).protocol)}catch{return!1}}var Tr=M.createContext(null);Tr.displayName="DataRouter";var vs=M.createContext(null);vs.displayName="DataRouterState";var OE=M.createContext(!1);function jy(){return M.useContext(OE)}var vE=M.createContext({isTransitioning:!1});vE.displayName="ViewTransition";var qy=M.createContext(new Map);qy.displayName="Fetchers";var Gy=M.createContext(null);Gy.displayName="Await";var Sn=M.createContext(null);Sn.displayName="Navigation";var wi=M.createContext(null);wi.displayName="Location";var jn=M.createContext({outlet:null,matches:[],isDataRoute:!1});jn.displayName="Route";var ch=M.createContext(null);ch.displayName="RouteError";var DE="REACT_ROUTER_ERROR",Qy="REDIRECT",Vy="ROUTE_ERROR_RESPONSE";function Xy(e){if(e.startsWith(`${DE}:${Qy}:{`))try{let n=JSON.parse(e.slice(28));if(typeof n=="object"&&n&&typeof n.status=="number"&&typeof n.statusText=="string"&&typeof n.location=="string"&&typeof n.reloadDocument=="boolean"&&typeof n.replace=="boolean")return n}catch{}}function Ky(e){if(e.startsWith(`${DE}:${Vy}:{`))try{let n=JSON.parse(e.slice(40));if(typeof n=="object"&&n&&typeof n.status=="number"&&typeof n.statusText=="string")return new Uy(n.status,n.statusText,n.data)}catch{}}function Zy(e,{relative:n}={}){lt(Bi(),"useHref() may be used only in the context of a component.");let{basename:u,navigator:i}=M.useContext(Sn),{hash:s,pathname:c,search:o}=Ui(e,{relative:n}),d=c;return u!=="/"&&(d=c==="/"?u:In([u,c])),i.createHref({pathname:d,search:o,hash:s})}function Bi(){return M.useContext(wi)!=null}function qn(){return lt(Bi(),"useLocation() may be used only in the context of a component."),M.useContext(wi).location}var LE="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function IE(e){M.useContext(Sn).static||M.useLayoutEffect(e)}function kE(){let{isDataRoute:e}=M.useContext(jn);return e?c_():Jy()}function Jy(){lt(Bi(),"useNavigate() may be used only in the context of a component.");let e=M.useContext(Tr),{basename:n,navigator:u}=M.useContext(Sn),{matches:i}=M.useContext(jn),{pathname:s}=qn(),c=JSON.stringify(AE(i)),o=M.useRef(!1);return IE(()=>{o.current=!0}),M.useCallback((E,p={})=>{if(Mn(o.current,LE),!o.current)return;if(typeof E=="number"){u.go(E);return}let g=sh(E,JSON.parse(c),s,p.relative==="path");e==null&&n!=="/"&&(g.pathname=g.pathname==="/"?n:In([n,g.pathname])),(p.replace?u.replace:u.push)(g,p.state,p)},[n,u,c,s,e])}M.createContext(null);function ME(){let{matches:e}=M.useContext(jn),n=e[e.length-1];return(n==null?void 0:n.params)??{}}function Ui(e,{relative:n}={}){let{matches:u}=M.useContext(jn),{pathname:i}=qn(),s=JSON.stringify(AE(u));return M.useMemo(()=>sh(e,JSON.parse(s),i,n==="path"),[e,s,i,n])}function Wy(e,n){return wE(e,n)}function wE(e,n,u){var w;lt(Bi(),"useRoutes() may be used only in the context of a component.");let{navigator:i}=M.useContext(Sn),{matches:s}=M.useContext(jn),c=s[s.length-1],o=c?c.params:{},d=c?c.pathname:"/",E=c?c.pathnameBase:"/",p=c&&c.route;{let L=p&&p.path||"";UE(d,!p||L.endsWith("*")||L.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${d}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. - -Please change the parent to .`)}let g=qn(),T;if(n){let L=typeof n=="string"?gr(n):n;lt(E==="/"||((w=L.pathname)==null?void 0:w.startsWith(E)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${E}" but pathname "${L.pathname}" was given in the \`location\` prop.`),T=L}else T=g;let _=T.pathname||"/",y=_;if(E!=="/"){let L=E.replace(/^\//,"").split("/");y="/"+_.replace(/^\//,"").split("/").slice(L.length).join("/")}let R=u&&u.state.matches.length?u.state.matches.map(L=>Object.assign(L,{route:u.manifest[L.route.id]||L.route})):TE(e,{pathname:y});Mn(p||R!=null,`No routes matched location "${T.pathname}${T.search}${T.hash}" `),Mn(R==null||R[R.length-1].route.element!==void 0||R[R.length-1].route.Component!==void 0||R[R.length-1].route.lazy!==void 0,`Matched leaf route at location "${T.pathname}${T.search}${T.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let I=a_(R&&R.map(L=>Object.assign({},L,{params:Object.assign({},o,L.params),pathname:In([E,i.encodeLocation?i.encodeLocation(L.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:L.pathname]),pathnameBase:L.pathnameBase==="/"?E:In([E,i.encodeLocation?i.encodeLocation(L.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:L.pathnameBase])})),s,u);return n&&I?M.createElement(wi.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...T},navigationType:"POP"}},I):I}function $y(){let e=s_(),n=Hy(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),u=e instanceof Error?e.stack:null,i="rgba(200,200,200, 0.5)",s={padding:"0.5rem",backgroundColor:i},c={padding:"2px 4px",backgroundColor:i},o=null;return console.error("Error handled by React Router default ErrorBoundary:",e),o=M.createElement(M.Fragment,null,M.createElement("p",null,"💿 Hey developer 👋"),M.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",M.createElement("code",{style:c},"ErrorBoundary")," or"," ",M.createElement("code",{style:c},"errorElement")," prop on your route.")),M.createElement(M.Fragment,null,M.createElement("h2",null,"Unexpected Application Error!"),M.createElement("h3",{style:{fontStyle:"italic"}},n),u?M.createElement("pre",{style:s},u):null,o)}var e_=M.createElement($y,null),BE=class extends M.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,n){return n.location!==e.location||n.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:n.error,location:n.location,revalidation:e.revalidation||n.revalidation}}componentDidCatch(e,n){this.props.onError?this.props.onError(e,n):console.error("React Router caught the following error during render",e)}render(){let e=this.state.error;if(this.context&&typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){const u=Ky(e.digest);u&&(e=u)}let n=e!==void 0?M.createElement(jn.Provider,{value:this.props.routeContext},M.createElement(ch.Provider,{value:e,children:this.props.component})):this.props.children;return this.context?M.createElement(t_,{error:e},n):n}};BE.contextType=OE;var df=new WeakMap;function t_({children:e,error:n}){let{basename:u}=M.useContext(Sn);if(typeof n=="object"&&n&&"digest"in n&&typeof n.digest=="string"){let i=Xy(n.digest);if(i){let s=df.get(n);if(s)throw s;let c=xE(i.location,u),o=c.absoluteURL||c.to;if(Yy(o))throw new Error("Invalid redirect location");if(NE&&!df.get(n))if(c.isExternal||i.reloadDocument)window.location.href=o;else{const d=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(c.to,{replace:i.replace}));throw df.set(n,d),d}return M.createElement("meta",{httpEquiv:"refresh",content:`0;url=${o}`})}}return e}function n_({routeContext:e,match:n,children:u}){let i=M.useContext(Tr);return i&&i.static&&i.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(i.staticContext._deepestRenderedBoundaryId=n.route.id),M.createElement(jn.Provider,{value:e},u)}function a_(e,n=[],u){let i=u==null?void 0:u.state;if(e==null){if(!i)return null;if(i.errors)e=i.matches;else if(n.length===0&&!i.initialized&&i.matches.length>0)e=i.matches;else return null}let s=e,c=i==null?void 0:i.errors;if(c!=null){let g=s.findIndex(T=>T.route.id&&(c==null?void 0:c[T.route.id])!==void 0);lt(g>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(c).join(",")}`),s=s.slice(0,Math.min(s.length,g+1))}let o=!1,d=-1;if(u&&i){o=i.renderFallback;for(let g=0;g=0?s=s.slice(0,d+1):s=[s[0]];break}}}}let E=u==null?void 0:u.onError,p=i&&E?(g,T)=>{var _,y;E(g,{location:i.location,params:((y=(_=i.matches)==null?void 0:_[0])==null?void 0:y.params)??{},pattern:Py(i.matches),errorInfo:T})}:void 0;return s.reduceRight((g,T,_)=>{let y,R=!1,I=null,w=null;i&&(y=c&&T.route.id?c[T.route.id]:void 0,I=T.route.errorElement||e_,o&&(d<0&&_===0?(UE("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),R=!0,w=null):d===_&&(R=!0,w=T.route.hydrateFallbackElement||null)));let L=n.concat(s.slice(0,_+1)),X=()=>{let q;return y?q=I:R?q=w:T.route.Component?q=M.createElement(T.route.Component,null):T.route.element?q=T.route.element:q=g,M.createElement(n_,{match:T,routeContext:{outlet:g,matches:L,isDataRoute:i!=null},children:q})};return i&&(T.route.ErrorBoundary||T.route.errorElement||_===0)?M.createElement(BE,{location:i.location,revalidation:i.revalidation,component:I,error:y,children:X(),routeContext:{outlet:null,matches:L,isDataRoute:!0},onError:p}):X()},null)}function oh(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function u_(e){let n=M.useContext(Tr);return lt(n,oh(e)),n}function r_(e){let n=M.useContext(vs);return lt(n,oh(e)),n}function i_(e){let n=M.useContext(jn);return lt(n,oh(e)),n}function fh(e){let n=i_(e),u=n.matches[n.matches.length-1];return lt(u.route.id,`${e} can only be used on routes that contain a unique "id"`),u.route.id}function l_(){return fh("useRouteId")}function s_(){var i;let e=M.useContext(ch),n=r_("useRouteError"),u=fh("useRouteError");return e!==void 0?e:(i=n.errors)==null?void 0:i[u]}function c_(){let{router:e}=u_("useNavigate"),n=fh("useNavigate"),u=M.useRef(!1);return IE(()=>{u.current=!0}),M.useCallback(async(s,c={})=>{Mn(u.current,LE),u.current&&(typeof s=="number"?await e.navigate(s):await e.navigate(s,{fromRouteId:n,...c}))},[e,n])}var Kp={};function UE(e,n,u){!n&&!Kp[e]&&(Kp[e]=!0,Mn(!1,u))}M.memo(o_);function o_({routes:e,manifest:n,future:u,state:i,isStatic:s,onError:c}){return wE(e,void 0,{manifest:n,state:i,isStatic:s,onError:c})}function fr(e){lt(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function f_({basename:e="/",children:n=null,location:u,navigationType:i="POP",navigator:s,static:c=!1,useTransitions:o}){lt(!Bi(),"You cannot render a inside another . You should never have more than one in your app.");let d=e.replace(/^\/*/,"/"),E=M.useMemo(()=>({basename:d,navigator:s,static:c,useTransitions:o,future:{}}),[d,s,c,o]);typeof u=="string"&&(u=gr(u));let{pathname:p="/",search:g="",hash:T="",state:_=null,key:y="default",mask:R}=u,I=M.useMemo(()=>{let w=Ea(p,d);return w==null?null:{location:{pathname:w,search:g,hash:T,state:_,key:y,mask:R},navigationType:i}},[d,p,g,T,_,y,i,R]);return Mn(I!=null,` is not able to match the URL "${p}${g}${T}" because it does not start with the basename, so the won't render anything.`),I==null?null:M.createElement(Sn.Provider,{value:E},M.createElement(wi.Provider,{children:n,value:I}))}function h_({children:e,location:n}){return Wy(Ff(e),n)}function Ff(e,n=[]){let u=[];return M.Children.forEach(e,(i,s)=>{if(!M.isValidElement(i))return;let c=[...n,s];if(i.type===M.Fragment){u.push.apply(u,Ff(i.props.children,c));return}lt(i.type===fr,`[${typeof i.type=="string"?i.type:i.type.name}] is not a component. All component children of must be a or `),lt(!i.props.index||!i.props.children,"An index route cannot have child routes.");let o={id:i.props.id||c.join("-"),caseSensitive:i.props.caseSensitive,element:i.props.element,Component:i.props.Component,index:i.props.index,path:i.props.path,middleware:i.props.middleware,loader:i.props.loader,action:i.props.action,hydrateFallbackElement:i.props.hydrateFallbackElement,HydrateFallback:i.props.HydrateFallback,errorElement:i.props.errorElement,ErrorBoundary:i.props.ErrorBoundary,hasErrorBoundary:i.props.hasErrorBoundary===!0||i.props.ErrorBoundary!=null||i.props.errorElement!=null,shouldRevalidate:i.props.shouldRevalidate,handle:i.props.handle,lazy:i.props.lazy};i.props.children&&(o.children=Ff(i.props.children,c)),u.push(o)}),u}var ms="get",ps="application/x-www-form-urlencoded";function Ds(e){return typeof HTMLElement<"u"&&e instanceof HTMLElement}function d_(e){return Ds(e)&&e.tagName.toLowerCase()==="button"}function m_(e){return Ds(e)&&e.tagName.toLowerCase()==="form"}function p_(e){return Ds(e)&&e.tagName.toLowerCase()==="input"}function E_(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function g_(e,n){return e.button===0&&(!n||n==="_self")&&!E_(e)}function zf(e=""){return new URLSearchParams(typeof e=="string"||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((n,u)=>{let i=e[u];return n.concat(Array.isArray(i)?i.map(s=>[u,s]):[[u,i]])},[]))}function T_(e,n){let u=zf(e);return n&&n.forEach((i,s)=>{u.has(s)||n.getAll(s).forEach(c=>{u.append(s,c)})}),u}var ss=null;function b_(){if(ss===null)try{new FormData(document.createElement("form"),0),ss=!1}catch{ss=!0}return ss}var y_=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function mf(e){return e!=null&&!y_.has(e)?(Mn(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${ps}"`),null):e}function __(e,n){let u,i,s,c,o;if(m_(e)){let d=e.getAttribute("action");i=d?Ea(d,n):null,u=e.getAttribute("method")||ms,s=mf(e.getAttribute("enctype"))||ps,c=new FormData(e)}else if(d_(e)||p_(e)&&(e.type==="submit"||e.type==="image")){let d=e.form;if(d==null)throw new Error('Cannot submit a + + + ); +} diff --git a/ui/chat/src/components/ChatInput.test.tsx b/ui/chat/src/components/ChatInput.test.tsx new file mode 100644 index 0000000000..9a1435fc8f --- /dev/null +++ b/ui/chat/src/components/ChatInput.test.tsx @@ -0,0 +1,457 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { + render, + screen, + fireEvent, + waitFor, + act, + within, +} from "@testing-library/react"; +import { ChatInput } from "./ChatInput"; +import { ChatProvider, useChatState } from "@/store/chat-store"; +import { + uploadAttachment, + deleteAttachment, + MAX_ATTACHMENTS_PER_TURN, +} from "@/api/attachments-api"; +import { ApiError } from "@/api/http"; + +/** + * ChatInput reports upload problems by posting into the transcript, which the + * widget renders and the composer does not — so mirror the messages here to + * assert on what the user would actually be told. + */ +function Transcript() { + const { messages } = useChatState(); + return ( +
+ {messages.map((m) => ( +

{m.content}

+ ))} +
+ ); +} + +function renderInput(props = {}) { + const onSend = vi.fn(); + const result = render( + + + + , + ); + return { ...result, onSend }; +} + +describe("ChatInput", () => { + it("renders a textarea and send button", () => { + renderInput(); + expect(screen.getByTestId("chat-input")).toBeInTheDocument(); + expect(screen.getByTestId("chat-send")).toBeInTheDocument(); + }); + + it("calls onSend on Enter key", () => { + const { onSend } = renderInput(); + const textarea = screen.getByTestId("chat-input"); + fireEvent.change(textarea, { target: { value: "Hello" } }); + fireEvent.keyDown(textarea, { key: "Enter", shiftKey: false }); + expect(onSend).toHaveBeenCalledWith("Hello", false); + }); + + it("does NOT send on Shift+Enter", () => { + const { onSend } = renderInput(); + const textarea = screen.getByTestId("chat-input"); + fireEvent.change(textarea, { target: { value: "Hello" } }); + fireEvent.keyDown(textarea, { key: "Enter", shiftKey: true }); + expect(onSend).not.toHaveBeenCalled(); + }); + + it("does NOT send when input is empty", () => { + const { onSend } = renderInput(); + const textarea = screen.getByTestId("chat-input"); + fireEvent.keyDown(textarea, { key: "Enter", shiftKey: false }); + expect(onSend).not.toHaveBeenCalled(); + }); + + it("does NOT send when disabled", () => { + const { onSend } = renderInput({ disabled: true }); + const textarea = screen.getByTestId("chat-input"); + fireEvent.change(textarea, { target: { value: "Hello" } }); + fireEvent.keyDown(textarea, { key: "Enter", shiftKey: false }); + expect(onSend).not.toHaveBeenCalled(); + }); + + it("clears input after send", () => { + renderInput(); + const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement; + fireEvent.change(textarea, { target: { value: "Hello" } }); + fireEvent.keyDown(textarea, { key: "Enter", shiftKey: false }); + expect(textarea.value).toBe(""); + }); +}); + +/* ─── Attachments ───────────────────────────── */ + +vi.mock("@/api/attachments-api", async () => { + const actual = await vi.importActual( + "@/api/attachments-api", + ); + return { + ...actual, + uploadAttachment: vi.fn(async (_convId: string, file: File) => ({ + storageRef: `ref-${file.name}`, + fileName: file.name, + mimeType: "application/pdf", + sizeBytes: 3, + forwardableInline: true, + })), + deleteAttachment: vi.fn(async () => {}), + }; +}); + +const pdf = (name: string) => + new File(["abc"], name, { type: "application/pdf" }); + +describe("ChatInput — attachments", () => { + beforeEach(() => { + vi.mocked(deleteAttachment).mockClear(); + }); + + async function attach(fileName = "invoice.pdf") { + const { onSend } = renderInput({ conversationId: "conv-1" }); + const input = screen.getByTestId("chat-file-input"); + fireEvent.change(input, { target: { files: [pdf(fileName)] } }); + return { onSend }; + } + + it("stages the upload instead of sending it as a message", async () => { + // The ref used to be stitched into the message text as "📎 name [ref:…]", + // which the backend never reads — the file silently never reached the model. + const { onSend } = await attach(); + + await screen.findByTestId("attachment-chip"); + expect(onSend).not.toHaveBeenCalled(); + }); + + it("shows the staged file name as a removable chip", async () => { + await attach("invoice.pdf"); + + const chip = await screen.findByTestId("attachment-chip"); + expect(chip).toHaveTextContent("invoice.pdf"); + expect(screen.getByTestId("attachment-remove")).toBeInTheDocument(); + }); + + it("removes a staged attachment when its remove button is clicked", async () => { + await attach(); + await screen.findByTestId("attachment-chip"); + + fireEvent.click(screen.getByTestId("attachment-remove")); + + expect(screen.queryByTestId("attachment-chip")).not.toBeInTheDocument(); + }); + + it("allows sending with only an attachment and no typed text", async () => { + const { onSend } = await attach(); + await screen.findByTestId("attachment-chip"); + + fireEvent.click(screen.getByTestId("chat-send")); + + expect(onSend).toHaveBeenCalled(); + }); + + it("deletes the blob server-side when a staged chip is removed", async () => { + // Removing the chip client-side only left the file in the store for the + // life of the conversation, still counting against the per-conversation + // file and byte quotas until no further attachment could be accepted. + await attach("invoice.pdf"); + await screen.findByTestId("attachment-chip"); + + fireEvent.click(screen.getByTestId("attachment-remove")); + + expect(vi.mocked(deleteAttachment)).toHaveBeenCalledWith( + "conv-1", + "ref-invoice.pdf", + ); + }); + + it("keeps removing the chip even when the server-side delete fails", async () => { + vi.mocked(deleteAttachment).mockRejectedValueOnce(new Error("offline")); + await attach(); + await screen.findByTestId("attachment-chip"); + + fireEvent.click(screen.getByTestId("attachment-remove")); + + expect(screen.queryByTestId("attachment-chip")).not.toBeInTheDocument(); + }); + + it("warns on a file that was stored but is too large to send to the model", async () => { + // The backend accepts up to max-size-bytes but only inlines up to + // max-forward-bytes. A file in between is dropped at forward time and the + // skip is recorded setPublic(false), so the upload response is the only + // chance we get to tell the user. + vi.mocked(uploadAttachment).mockResolvedValueOnce({ + storageRef: "ref-big", + fileName: "huge.pdf", + mimeType: "application/pdf", + sizeBytes: 14_000_000, + forwardableInline: false, + }); + await attach("huge.pdf"); + + expect(await screen.findByTestId("attachment-warn")).toHaveTextContent( + "too large to send directly", + ); + expect(await screen.findByTestId("attachment-chip")).toHaveClass( + "chat-attachments__chip--warn", + ); + }); + + it("does NOT warn when the backend omits forwardableInline", async () => { + // An older backend simply does not send the field. Absence is not a denial. + vi.mocked(uploadAttachment).mockResolvedValueOnce({ + storageRef: "ref-x", + fileName: "x.pdf", + mimeType: "application/pdf", + sizeBytes: 3, + }); + await attach("x.pdf"); + await screen.findByTestId("attachment-chip"); + + expect(screen.queryByTestId("attachment-warn")).not.toBeInTheDocument(); + }); + + it("marks the file input as multi-select", async () => { + // Asserted directly because jsdom does not enforce `multiple`: + // fireEvent.change can hand a component several files whether or not the + // attribute is present, so the batch tests below would pass without it. + // This is the only assertion that pins the attribute that makes the real + // OS picker allow more than one file. + renderInput({ conversationId: "conv-1" }); + + expect(screen.getByTestId("chat-file-input")).toHaveAttribute("multiple"); + }); + + it("uploads every file from a single multi-file pick", async () => { + renderInput({ conversationId: "conv-1" }); + fireEvent.change(screen.getByTestId("chat-file-input"), { + target: { files: [pdf("a.pdf"), pdf("b.pdf"), pdf("c.pdf")] }, + }); + + await waitFor(() => + expect(screen.getAllByTestId("attachment-chip")).toHaveLength(3), + ); + }); + + it("caps a multi-file pick at the per-turn limit and says so", async () => { + // The backend drops the excess server-side, so accepting all seven here + // would tell the user files were sent that never reached the agent. + renderInput({ conversationId: "conv-1" }); + fireEvent.change(screen.getByTestId("chat-file-input"), { + target: { files: Array.from({ length: 7 }, (_, i) => pdf(`f${i}.pdf`)) }, + }); + + await waitFor(() => + expect(screen.getAllByTestId("attachment-chip")).toHaveLength( + MAX_ATTACHMENTS_PER_TURN, + ), + ); + // Names them: slice() keeps OS-determined FileList order, so a bare count + // would leave the user guessing which two were dropped. + expect( + within(screen.getByTestId("transcript")).getByText( + /Not attached: f5\.pdf, f6\.pdf/, + ), + ).toBeInTheDocument(); + }); + + it("shows a placeholder chip while the upload is still in flight", async () => { + let release: (v: unknown) => void = () => {}; + vi.mocked(uploadAttachment).mockImplementationOnce( + () => new Promise((res) => { release = res; }) as never, + ); + renderInput({ conversationId: "conv-1" }); + fireEvent.change(screen.getByTestId("chat-file-input"), { + target: { files: [pdf("slow.pdf")] }, + }); + + expect( + await screen.findByTestId("attachment-chip-uploading"), + ).toHaveTextContent("slow.pdf"); + + await act(async () => { + release({ + storageRef: "ref-slow", + fileName: "slow.pdf", + mimeType: "application/pdf", + sizeBytes: 3, + forwardableInline: true, + }); + }); + expect( + screen.queryByTestId("attachment-chip-uploading"), + ).not.toBeInTheDocument(); + }); + + it("surfaces the server's own reason for a rejection", async () => { + // ATTACHMENT_REJECTED is a catch-all — a quota rejection used to be + // reported to the user as an unaccepted file type. + vi.mocked(uploadAttachment).mockRejectedValueOnce( + new ApiError( + 400, + JSON.stringify({ + error: "Conversation attachment limit reached (50). Delete some attachments first.", + code: "ATTACHMENT_REJECTED", + }), + "Attachment upload failed", + ), + ); + await attach("late.pdf"); + + await waitFor(() => + expect( + within(screen.getByTestId("transcript")).getByText( + /Delete some attachments first/, + ), + ).toBeInTheDocument(), + ); + }); + + it.each([ + ["a bare 413 with no JSON envelope", new ApiError(413, "413", "x"), /too large to upload/], + ["a 400 ATTACHMENT_TOO_LARGE", new ApiError(400, JSON.stringify({ error: "File too large: 9", code: "ATTACHMENT_TOO_LARGE" }), "x"), /too large to upload/], + ["a 403", new ApiError(403, "", "x"), /not allowed to attach/], + ["a 401", new ApiError(401, "", "x"), /not allowed to attach/], + ["a non-ApiError", new Error("socket hang up"), /Failed to upload/], + ])("explains %s", async (_label, err, expected) => { + vi.mocked(uploadAttachment).mockRejectedValueOnce(err); + await attach("doc.pdf"); + + await waitFor(() => + expect( + within(screen.getByTestId("transcript")).getByText(expected), + ).toBeInTheDocument(), + ); + }); + + it("refuses a further pick once the cap is already full", async () => { + // The batch-overflow path and the already-full path are different branches. + renderInput({ conversationId: "conv-1" }); + fireEvent.change(screen.getByTestId("chat-file-input"), { + target: { files: Array.from({ length: 5 }, (_, i) => pdf(`f${i}.pdf`)) }, + }); + await waitFor(() => + expect(screen.getAllByTestId("attachment-chip")).toHaveLength(5), + ); + + fireEvent.change(screen.getByTestId("chat-file-input"), { + target: { files: [pdf("one-too-many.pdf")] }, + }); + + await waitFor(() => + expect( + within(screen.getByTestId("transcript")).getByText( + /You can attach at most 5 files per message\.$/, + ), + ).toBeInTheDocument(), + ); + expect(screen.getAllByTestId("attachment-chip")).toHaveLength(5); + }); + + it("keeps the live region mounted before anything is attached", async () => { + // A live region that appears together with its content is not announced, + // so this one must already exist while the composer is empty. + renderInput({ conversationId: "conv-1" }); + + expect(screen.getByTestId("chat-live")).toHaveAttribute("aria-live", "polite"); + expect(screen.queryByTestId("attachment-chips")).not.toBeInTheDocument(); + }); + + it("announces an attachment and its removal", async () => { + await attach("invoice.pdf"); + await screen.findByTestId("attachment-chip"); + expect(screen.getByTestId("chat-live")).toHaveTextContent("invoice.pdf attached."); + + fireEvent.click(screen.getByTestId("attachment-remove")); + + expect(screen.getByTestId("chat-live")).toHaveTextContent("invoice.pdf removed."); + }); + + it("announces an upload failure, which only ever REMOVES a chip", async () => { + // The default aria-relevant does not cover removals, so a failure had no + // other route to a screen reader. + vi.mocked(uploadAttachment).mockRejectedValueOnce( + new ApiError(400, JSON.stringify({ error: "Bad type", code: "ATTACHMENT_REJECTED" }), "x"), + ); + await attach("virus.exe"); + + await waitFor(() => + expect(screen.getByTestId("chat-live")).toHaveTextContent("virus.exe was rejected"), + ); + expect(screen.queryByTestId("attachment-chip-uploading")).not.toBeInTheDocument(); + }); + + it("refuses to send while an upload is still in flight", async () => { + // Sending here would go out with NO attachment context, then CLEAR_ATTACHMENTS + // would fire and the upload would land as a chip belonging to the NEXT turn — + // leaving the user looking at a file they believe they already sent. + let release: (v: unknown) => void = () => {}; + vi.mocked(uploadAttachment).mockImplementationOnce( + () => new Promise((res) => { release = res; }) as never, + ); + const { onSend } = renderInput({ conversationId: "conv-1" }); + fireEvent.change(screen.getByTestId("chat-file-input"), { + target: { files: [pdf("slow.pdf")] }, + }); + await screen.findByTestId("attachment-chip-uploading"); + + const input = screen.getByTestId("chat-input"); + fireEvent.change(input, { target: { value: "here you go" } }); + fireEvent.keyDown(input, { key: "Enter", shiftKey: false }); + + expect(onSend).not.toHaveBeenCalled(); + expect(screen.getByTestId("chat-send")).toBeDisabled(); + + await act(async () => { + release({ + storageRef: "ref-slow", fileName: "slow.pdf", + mimeType: "application/pdf", sizeBytes: 3, forwardableInline: true, + }); + }); + fireEvent.keyDown(input, { key: "Enter", shiftKey: false }); + expect(onSend).toHaveBeenCalled(); + }); + + it("deletes against the conversation the upload reported, not the current one", async () => { + // An upload started before New Conversation lands afterwards and stages + // into the fresh composer. Deleting it against the new id is refused by the + // store's owner check, and the original blob leaks against the very quota + // this is meant to protect. + vi.mocked(uploadAttachment).mockResolvedValueOnce({ + storageRef: "ref-old", + fileName: "old.pdf", + mimeType: "application/pdf", + sizeBytes: 3, + conversationId: "conv-OLD", + }); + renderInput({ conversationId: "conv-NEW" }); + fireEvent.change(screen.getByTestId("chat-file-input"), { + target: { files: [pdf("old.pdf")] }, + }); + await screen.findByTestId("attachment-chip"); + + fireEvent.click(screen.getByTestId("attachment-remove")); + + expect(vi.mocked(deleteAttachment)).toHaveBeenCalledWith("conv-OLD", "ref-old"); + }); + + it("moves focus to the attach button when the last chip is removed", async () => { + await attach(); + await screen.findByTestId("attachment-chip"); + + fireEvent.click(screen.getByTestId("attachment-remove")); + + await waitFor(() => + expect(screen.getByTestId("chat-attach-btn")).toHaveFocus(), + ); + }); +}); diff --git a/ui/chat/src/components/ChatInput.tsx b/ui/chat/src/components/ChatInput.tsx new file mode 100644 index 0000000000..977d7486ae --- /dev/null +++ b/ui/chat/src/components/ChatInput.tsx @@ -0,0 +1,463 @@ +/* ────────────────────────────────────────────── + ChatInput — Auto-growing textarea + send button + With 🔒 secret mode toggle for client-initiated secret input. + ────────────────────────────────────────────── */ + +import { + useState, + useRef, + useCallback, + useEffect, + useMemo, + type KeyboardEvent, +} from "react"; +import { useChatState, useChatDispatch } from "@/store/chat-store"; +import { + uploadAttachment, + deleteAttachment, + MAX_ATTACHMENTS_PER_TURN, + type AttachmentResult, +} from "@/api/attachments-api"; +import { ApiError, errorPayload } from "@/api/http"; + +/** Turn an upload failure into copy that names the actual reason. */ +function describeUploadFailure(err: unknown, fileName: string): string { + if (err instanceof ApiError) { + const { code, message } = errorPayload(err); + // Quarkus enforces its own request-body cap before the attachment layer + // runs, so an oversize upload can arrive as a bare 413 with no envelope. + if (err.status === 413 || code === "ATTACHMENT_TOO_LARGE") { + return `${fileName} is too large to upload.`; + } + if (err.status === 401 || err.status === 403) { + return `You are not allowed to attach files to this conversation.`; + } + // ATTACHMENT_REJECTED is a catch-all: a declared-vs-detected MIME mismatch + // (there is no type allowlist), the per-conversation file-count and byte + // quotas, empty files — and on Postgres any wrapped SQLException, so an + // outage arrives dressed as a rejection too. Only the server's own text + // separates them, so prefer it to a guess. + if (message) return `${fileName} was rejected — ${message}`; + if (code) return `${fileName} was rejected.`; + } + return `Failed to upload ${fileName}.`; +} + +interface ChatInputProps { + onSend: (message: string, isSecret?: boolean) => void; + disabled?: boolean; + conversationId?: string | null; +} + +export function ChatInput({ onSend, disabled, conversationId }: ChatInputProps) { + const { isProcessing, config, isSecretMode, pendingAttachments, restoreDraft } = + useChatState(); + const dispatch = useChatDispatch(); + const [value, setValue] = useState(""); + /** + * Files currently uploading, shown as placeholder chips ahead of the staged + * ones. Without them a large file over a slow link leaves the composer + * looking untouched for several seconds. Carries a stable id so resolving a + * middle entry does not remount every chip after it. + * + * Declared up here because sending is blocked while it is non-empty. + */ + const [uploading, setUploading] = useState<{ id: number; name: string }[]>([]); + const isUploading = uploading.length > 0; + const [secretVisible, setSecretVisible] = useState(false); + const textareaRef = useRef(null); + + // A turn the server refused (409) was never consumed — put the text back so + // the user does not have to retype it. + useEffect(() => { + if (restoreDraft === null) return; + setValue(restoreDraft); + dispatch({ type: "CLEAR_RESTORE_DRAFT" }); + textareaRef.current?.focus(); + }, [restoreDraft, dispatch]); + + const handleSend = useCallback(() => { + const trimmed = value.trim(); + // An attachment on its own is a valid turn — the file is the message. + const hasAttachments = pendingAttachments.length > 0; + // Sending mid-upload would go out WITHOUT the file: the staged list is + // still empty, CLEAR_ATTACHMENTS then fires, and the upload lands + // afterwards as a chip for the NEXT turn — leaving the user looking at an + // attachment they believe they already sent. + if ((!trimmed && !hasAttachments) || disabled || isProcessing || isUploading) { + return; + } + onSend(trimmed, isSecretMode); + setValue(""); + if (isSecretMode) { + dispatch({ type: "TOGGLE_SECRET_MODE" }); + setSecretVisible(false); + } + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + } + }, [ + value, + disabled, + isProcessing, + isUploading, + isSecretMode, + onSend, + dispatch, + pendingAttachments.length, + ]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }, + [handleSend], + ); + + const handleInput = useCallback(() => { + const el = textareaRef.current; + if (!el) return; + el.style.height = "auto"; + el.style.height = `${Math.min(el.scrollHeight, 150)}px`; + }, []); + + const toggleSecretMode = useCallback(() => { + dispatch({ type: "TOGGLE_SECRET_MODE" }); + setSecretVisible(false); + }, [dispatch]); + + const canSend = + (value.trim().length > 0 || pendingAttachments.length > 0) && + !disabled && + !isProcessing && + !isUploading; + + // ── Attachment upload ── + const fileInputRef = useRef(null); + const attachBtnRef = useRef(null); + const chipsRef = useRef(null); + const uploadSeq = useRef(0); + const atCapacity = + pendingAttachments.length + uploading.length >= MAX_ATTACHMENTS_PER_TURN; + + /** + * Assistive-tech announcements. + * + * This region is ALWAYS mounted, deliberately. A live region has to exist in + * the accessibility tree before its content changes — one that appears + * already populated is not announced — so the conditional chip strip could + * never announce its own first chip. Upload failures are worse still: they + * only ever *remove* a placeholder, and the default `aria-relevant` does not + * cover removals, so nothing at all was being said. + */ + const [live, setLive] = useState(""); + const announce = useCallback((text: string) => { + // Re-setting the identical string leaves the DOM untouched, and an + // unchanged region is not re-announced. Alternate an invisible suffix so + // the same message twice in a row still speaks. + setLive((prev) => (prev === text ? `${text}​` : text)); + }, []); + + /** Post a problem into the transcript and speak it. */ + const notify = useCallback( + (sentence: string) => { + dispatch({ + type: "ADD_MESSAGE", + message: { + id: `error-${Date.now()}-${Math.random()}`, + role: "agent", + content: `⚠️ ${sentence}`, + timestamp: Date.now(), + }, + }); + announce(sentence); + }, + [dispatch, announce], + ); + + /** + * Removing a chip unmounts the focused button. Without this, focus falls to + * and keyboard/screen-reader users lose their place in the composer. + */ + const focusIdxRef = useRef(null); + useEffect(() => { + const idx = focusIdxRef.current; + if (idx === null) return; + focusIdxRef.current = null; + const btns = chipsRef.current?.querySelectorAll( + '[data-testid="attachment-remove"]', + ); + if (btns && btns.length > 0) btns[Math.min(idx, btns.length - 1)]?.focus(); + else attachBtnRef.current?.focus(); + }, [pendingAttachments]); + + const handleAttach = useCallback( + async (e: React.ChangeEvent) => { + const picked = Array.from(e.target.files ?? []); + // Reset immediately so re-picking the same file still fires a change. + if (fileInputRef.current) fileInputRef.current.value = ""; + if (!picked.length || !conversationId) return; + + // Room is computed once for the whole batch. Checking one file at a time + // would let a multi-file pick sail past the cap, and the backend drops + // the excess server-side — so the UI must not pretend it was sent. + const room = + MAX_ATTACHMENTS_PER_TURN - pendingAttachments.length - uploading.length; + if (room <= 0) { + notify( + `You can attach at most ${MAX_ATTACHMENTS_PER_TURN} files per message.`, + ); + return; + } + const accepted = picked.slice(0, room); + if (accepted.length < picked.length) { + // Name the casualties: slice() keeps FileList order, which is + // OS-determined, so "2 not attached" leaves the user guessing which. + notify( + `You can attach at most ${MAX_ATTACHMENTS_PER_TURN} files per message. ` + + `Not attached: ${picked + .slice(room) + .map((f) => f.name) + .join(", ")}`, + ); + } + + const staged = accepted.map((file) => ({ + file, + id: (uploadSeq.current += 1), + })); + setUploading((prev) => [ + ...prev, + ...staged.map(({ id, file }) => ({ id, name: file.name })), + ]); + await Promise.all( + staged.map(async ({ file, id }) => { + try { + const result = await uploadAttachment(conversationId, file); + // Stage it. The ref reaches the agent as an attachment_N context + // entry when the next message is sent — embedding it in the message + // text was silently ignored by the backend. + dispatch({ type: "ADD_ATTACHMENT", attachment: result }); + announce( + result.forwardableInline === false + ? `${file.name} attached, but it is too large to send directly — the assistant may not be able to read it.` + : `${file.name} attached.`, + ); + } catch (err) { + notify(describeUploadFailure(err, file.name)); + } finally { + setUploading((prev) => prev.filter((u) => u.id !== id)); + } + }), + ); + }, + [ + conversationId, + dispatch, + notify, + announce, + pendingAttachments.length, + uploading.length, + ], + ); + + /** File names staged more than once, so their remove buttons can disambiguate. */ + const duplicateNames = useMemo(() => { + const seen = new Set(); + const dupes = new Set(); + for (const a of pendingAttachments) { + if (seen.has(a.fileName)) dupes.add(a.fileName); + else seen.add(a.fileName); + } + return dupes; + }, [pendingAttachments]); + + const handleRemoveAttachment = useCallback( + (a: AttachmentResult, index: number) => { + const { storageRef, fileName } = a; + focusIdxRef.current = index; + // Delete server-side too. Only unsent attachments are removable here, so + // this can never orphan a blob a sent turn still references — whereas + // skipping it leaves the file in the store for the life of the + // conversation, counting against the per-conversation file and byte + // quotas until the user can no longer attach anything at all. + // Fire-and-forget: the chip goes regardless of what the server says. + // Prefer the id the upload reported over whatever is current now. + const owner = a.conversationId ?? conversationId; + if (owner) { + deleteAttachment(owner, storageRef).catch(() => {}); + } + dispatch({ type: "REMOVE_ATTACHMENT", storageRef }); + announce(`${fileName} removed.`); + }, + [conversationId, dispatch, announce], + ); + + return ( +
+ {/* Always mounted — see `announce`. */} +
+ {live} +
+ + {(pendingAttachments.length > 0 || isUploading) && ( +
+ {pendingAttachments.map((a, i) => ( + + + {a.fileName} + + + {/* Last, so it takes its own row below the name — see the CSS. */} + {a.forwardableInline === false && ( + + too large to send directly + + )} + + ))} + {uploading.map((u) => ( + + + {u.name} + + {/* The ⏳ is the only visual difference from a staged chip. */} + Uploading + + ))} +
+ )} +
+ {/* Hidden file input for attachments */} + + {/* 📎 Attach button */} + + {/* 🔒 Secret mode toggle */} + + + {isSecretMode ? ( + /* Secret mode: password input with eye toggle */ +
+ setValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Enter secret value..." + disabled={disabled} + className="chat-input__secret-field" + autoComplete="off" + /> + +
+ ) : ( + /* Normal mode: auto-growing textarea */ +