diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f5e5becca --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +src/inspect_scout/_view/dist/** filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index cb8390e39..cddf693cd 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -125,7 +125,7 @@ jobs: run: python scripts/export_openapi_schema.py - name: Check schema is up-to-date run: | - if ! git diff --exit-code src/inspect_scout/_view/www/openapi.json + if ! git diff --exit-code src/inspect_scout/_view/openapi.json then echo "❌ openapi.json is out of sync with Python API code!" echo "" @@ -136,208 +136,83 @@ jobs: exit 1 fi - js-lint: + submodule-on-main: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - package_json_file: src/inspect_scout/_view/www/package.json - - - name: Install Node.js - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: pnpm - cache-dependency-path: src/inspect_scout/_view/www/pnpm-lock.yaml - - - name: Install dependencies - working-directory: src/inspect_scout/_view/www - run: pnpm install --frozen-lockfile - - - name: Check linting - working-directory: src/inspect_scout/_view/www - run: pnpm lint - - js-typecheck: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - package_json_file: src/inspect_scout/_view/www/package.json - - - name: Install Node.js - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: pnpm - cache-dependency-path: src/inspect_scout/_view/www/pnpm-lock.yaml - - - name: Install dependencies - working-directory: src/inspect_scout/_view/www - run: pnpm install --frozen-lockfile - - - name: Check typechecking - working-directory: src/inspect_scout/_view/www - run: pnpm typecheck + - name: Verify submodule commit is on ts-mono main + run: | + SUBMODULE_SHA=$(git ls-tree HEAD src/inspect_scout/_view/ts-mono | awk '{print $3}') + echo "Submodule points to: $SUBMODULE_SHA" + + git fetch https://github.com/meridianlabs-ai/ts-mono.git main + MAIN_SHA=$(git rev-parse FETCH_HEAD) + echo "ts-mono main is at: $MAIN_SHA" + + if git merge-base --is-ancestor "$SUBMODULE_SHA" "$MAIN_SHA"; then + echo "✓ Submodule commit is on ts-mono main" + else + echo "✗ Submodule commit $SUBMODULE_SHA is not on ts-mono main." + echo " Merge the submodule PR first, then update the pointer (see docs/submodule-guide.md step 6)." + exit 1 + fi - js-format: + js-dist-validation: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 with: - package_json_file: src/inspect_scout/_view/www/package.json - - - name: Install Node.js - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: pnpm - cache-dependency-path: src/inspect_scout/_view/www/pnpm-lock.yaml - - - name: Install dependencies - working-directory: src/inspect_scout/_view/www - run: pnpm install --frozen-lockfile - - - name: Check formatting - working-directory: src/inspect_scout/_view/www - run: pnpm format:check - - js-build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 + submodules: true - uses: pnpm/action-setup@v4 with: - package_json_file: src/inspect_scout/_view/www/package.json + package_json_file: src/inspect_scout/_view/ts-mono/package.json - name: Install Node.js uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm - cache-dependency-path: src/inspect_scout/_view/www/pnpm-lock.yaml + cache-dependency-path: src/inspect_scout/_view/ts-mono/pnpm-lock.yaml - name: Install dependencies - working-directory: src/inspect_scout/_view/www + working-directory: src/inspect_scout/_view/ts-mono run: pnpm install --frozen-lockfile - name: Generate TypeScript types from OpenAPI - working-directory: src/inspect_scout/_view/www - run: pnpm types:generate + working-directory: src/inspect_scout/_view/ts-mono + run: pnpm --filter scout types:generate - name: Check generated types are up-to-date - working-directory: src/inspect_scout/_view/www + working-directory: src/inspect_scout/_view/ts-mono run: | - if ! git diff --exit-code src/types/generated.ts + if ! git diff --exit-code then echo "❌ generated.ts is out of sync with openapi.json!" echo "" echo "TypeScript types need to be regenerated. Run:" - echo " cd src/inspect_scout/_view/www" - echo " pnpm types:generate" + echo " cd src/inspect_scout/_view/ts-mono" + echo " pnpm --filter scout types:generate" echo "" - echo "Then commit the updated src/types/generated.ts file." + echo "Then commit the updated generated.ts file." exit 1 fi - - name: Build package - working-directory: src/inspect_scout/_view/www + - name: Build + working-directory: src/inspect_scout/_view/ts-mono run: pnpm build - - name: Check build output is clean - working-directory: src/inspect_scout/_view/www + - name: Check dist is up-to-date run: | - if ! git diff --exit-code + if ! git diff --exit-code src/inspect_scout/_view/dist then - echo "❌ Build produced unexpected changes!" + echo "❌ dist/ is out of sync with the frontend source!" + echo "" + echo "The build output has changed. Run:" + echo " cd src/inspect_scout/_view/ts-mono" + echo " pnpm build" echo "" - echo "Files changed during build:" - git diff --name-only + echo "Then commit the updated dist/ directory." exit 1 fi - - js-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - package_json_file: src/inspect_scout/_view/www/package.json - - - name: Install Node.js - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: pnpm - cache-dependency-path: src/inspect_scout/_view/www/pnpm-lock.yaml - - - name: Install dependencies - working-directory: src/inspect_scout/_view/www - run: pnpm install --frozen-lockfile - - - name: Run tests - working-directory: src/inspect_scout/_view/www - run: pnpm test - - js-e2e: - continue-on-error: true # TODO: remove once e2e tests prove stable - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - package_json_file: src/inspect_scout/_view/www/package.json - - - name: Install Node.js - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: pnpm - cache-dependency-path: src/inspect_scout/_view/www/pnpm-lock.yaml - - - name: Install dependencies - working-directory: src/inspect_scout/_view/www - run: pnpm install --frozen-lockfile - - - name: Get Playwright version - id: pw-version - working-directory: src/inspect_scout/_view/www - run: echo "version=$(pnpm exec playwright --version)" >> "$GITHUB_OUTPUT" - - - name: Cache Playwright browsers - id: pw-cache - uses: actions/cache@v4 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }} - - - name: Install Playwright browsers - if: steps.pw-cache.outputs.cache-hit != 'true' - working-directory: src/inspect_scout/_view/www - run: pnpm exec playwright install --with-deps chromium - - - name: Install Playwright system deps (on cache hit) - if: steps.pw-cache.outputs.cache-hit == 'true' - working-directory: src/inspect_scout/_view/www - run: pnpm exec playwright install-deps chromium - - - name: Run e2e tests - working-directory: src/inspect_scout/_view/www - run: pnpm e2e - - - name: Upload test results - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: playwright-report - path: src/inspect_scout/_view/www/playwright-report/ - retention-days: 7 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 6635d43b1..e1b75debc 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -24,7 +24,7 @@ jobs: environment: npm defaults: run: - working-directory: src/inspect_scout/_view/www + working-directory: src/inspect_scout/_view/ts-mono/apps/scout permissions: contents: read id-token: write @@ -33,11 +33,10 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + submodules: true - name: Set up pnpm uses: pnpm/action-setup@v4 - with: - version: 9 - name: Set up Node.js uses: actions/setup-node@v4 @@ -45,18 +44,22 @@ jobs: node-version: "22.x" registry-url: "https://registry.npmjs.org" cache: "pnpm" - cache-dependency-path: "src/inspect_scout/_view/www/pnpm-lock.yaml" + cache-dependency-path: src/inspect_scout/_view/ts-mono/pnpm-lock.yaml - name: Upgrade npm (for Trusted Publishing) run: | npm i -g npm@^11.5.1 - npm --version - + npm --version + - name: Install dependencies + working-directory: src/inspect_scout/_view/ts-mono run: pnpm install --frozen-lockfile - - name: Sync version from git tag - run: npm version from-git --no-git-tag-version + - name: Prepare package for publishing + run: | + npm pkg set name="@meridianlabs/inspect-scout-viewer" + npm pkg delete private + npm version from-git --no-git-tag-version - name: Bump to prerelease version if: ${{ inputs.prerelease }} @@ -68,6 +71,7 @@ jobs: npm version "${NEW_VERSION}-beta.${TIMESTAMP}" --no-git-tag-version - name: Run quality checks + working-directory: src/inspect_scout/_view/ts-mono run: pnpm check - name: Build library for prerelease diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index cbc28e51e..e40e03744 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -21,6 +21,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # Get all tags for version detection + lfs: true - name: Set up Python uses: actions/setup-python@v5 diff --git a/.gitignore b/.gitignore index aa3e52eb3..54c062d0d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,10 @@ MANIFEST *.manifest *.spec +# Built view maps +/src/inspect_scout/_view/dist/assets/*.js.map + + # Installer logs pip-log.txt pip-delete-this-directory.txt @@ -210,6 +214,7 @@ src/inspect_scout/_version.py # Inspect logs and scans /logs/ +/logs-*/ /scans/ # Scout local configuration (developer-only, not checked in) @@ -226,9 +231,6 @@ scout.local.yaml # node /src/inspect_scout/_view/www/node_modules -# dist builds -/src/inspect_scout/_view/www/*.tgz - # test data /examples/test-db diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..756cb6da2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/inspect_scout/_view/ts-mono"] + path = src/inspect_scout/_view/ts-mono + url = https://github.com/meridianlabs-ai/ts-mono.git diff --git a/AGENTS.md b/AGENTS.md index 30b66c123..b4872e591 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Repository Structure - **Python package** (`src/inspect_scout/`) - Core library for analyzing LLM evaluation transcripts. Provides a CLI, programmatic API, and FastAPI server. Handles transcript databases, scanners, validation, and results. -- **React frontend** (`src/inspect_scout/_view/www/`) - Web UI for viewing scan results, exploring transcripts, and managing projects. +- **React frontend** (`src/inspect_scout/_view/ts-mono/`) - TypeScript monorepo (Turborepo + pnpm workspaces) for the web UI, embedded as a git submodule. See its own [CLAUDE.md](src/inspect_scout/_view/ts-mono/CLAUDE.md) and [submodule guide](src/inspect_scout/_view/ts-mono/docs/submodule-guide.md). ## Principles @@ -18,6 +18,8 @@ ### Testing - Test observable behavior, not internal implementation details +- Do not test things that are enforced by the type system +- Test through the narrowest public API that covers the behavior - Be efficient; avoid duplicate coverage - Prefer data/table driven tests for maintainability - Tests must be isolated; no shared mutable state or order dependencies @@ -45,8 +47,8 @@ Architecture and design decisions in `/design/`. - [Async generator semantics](design/generator-iterator.md) - [Multi-process concurrency](design/mp.md) - [Validation data structures](design/validation.md) -- [React Query patterns](src/inspect_scout/_view/www/design/react-query.md) -- [Frontend specific testing](src/inspect_scout/_view/www/design/front-end-testing.md) +- [React Query patterns](src/inspect_scout/_view/ts-mono/apps/scout/design/react-query.md) +- [Frontend specific testing](src/inspect_scout/_view/ts-mono/apps/scout/design/front-end-testing.md) ## Python @@ -79,7 +81,9 @@ Directory: `src/inspect_scout/` ## TypeScript -Directory: `src/inspect_scout/_view/www/` (run all commands from here) +Directory: `src/inspect_scout/_view/ts-mono/` (run all commands from here) + +See the frontend's own [CLAUDE.md](src/inspect_scout/_view/ts-mono/CLAUDE.md) for full details. ### Setup ```bash @@ -90,18 +94,14 @@ pnpm install ### Scripts | Command | Description | |---------|-------------| -| `pnpm check` | Run all checks (lint, format, typecheck) | +| `pnpm check` | Run all checks (lint, format, typecheck) via Turborepo | | `pnpm dev` | Start dev server (user typically has this running—don't start) | | `pnpm watch` | Watch mode (user typically has this running—don't start) | | `pnpm build` | Production build | | `pnpm test` | Run unit/integration tests | -| `pnpm e2e` | Run Playwright e2e tests | -| `pnpm e2e:ui` | Run e2e tests with interactive UI | -| `pnpm e2e:headed` | Run e2e tests in headed browser | -| `pnpm lint` | Lint code | -| `pnpm lint:fix` | Lint and auto-fix | +| `pnpm lint` | Lint all packages | +| `pnpm typecheck` | Type-check all packages | | `pnpm format` | Format code | -| `pnpm typecheck` | Type check | ### Style - Strict mode enabled; no `any`, no type assertions @@ -110,6 +110,7 @@ pnpm install ### Common Pitfalls - Use pnpm, not npm—this project uses pnpm exclusively - Hook tests don't need JSX—use `.test.ts` not `.test.tsx`; see `useMapAsyncData.test.ts` +- Run `pnpm check` to type check, lint, and otherwise check your code quality - Run `pnpm build` before committing (not just `pnpm check`)—we ship the built .js code diff --git a/CHANGELOG.md b/CHANGELOG.md index 44a190b03..7a5021dde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,42 @@ +## 0.4.22 (20 March 2026) + +- Transcripts: Use `events_data` to reduce memory and storage requirements of `events`. +- Summary: Count only positive values in resultset aggregation. +- Summary: Reset summary state on new scan init to prevent accumulation across scans. +- Recorder: Preserve error file on scan resume instead of truncating. + +## 0.4.21 (16 March 2026) + +- Scanner as scorer: Forward timelines from eval sample to scanner if requested. +- Scout View: Correctly display timelines in results view. +- Scout View: Show dictionary-based metriecs in scanner sidebar. +- Scout View: Bundle `dist/` assets during packaging. + +## 0.4.20 (16 March 2026) + +- LLM Scanner: Automatic transcript segmentation by context window with configurable compaction handling. +- LLM Scanner: `AnswerMultiLabel(allow_none=True)` lets the model respond with `ANSWER: NONE` when no labels apply. +- Scanner Tools: `generate_answer()` automatically retries with format feedback when the model's response can't be parsed. +- Scanner Tools: `message_numbering()`, `scanner_prompt()`, `generate_answer()`, and `parse_answer()` functions for building custom scanners with fine-grained control. +- Scanner Tools: `ResultReducer` for reducing results from multiple transcript segments into single results, with built-in majority and LLM-based reducers. +- Scanner Tools: `transcript_messages()`, `segment_messages()`, and `span_messages()` functions for extracting and segmenting transcript messages. +- Transcript DB: `claude_code()` source for importing transcripts from Claude Code session logs. Supports filtering by project, session, and time range, session merging, and image extraction. +- CLI: `scout import` command for importing transcripts from registered sources into Scout projects. +- Serialization: Use `pa.large_string` for string types to support larger column/file sizes. +- Multiprocessing: Improve handling of model instances with multiprocessing serialization. +- Transcripts: unthin `target` and and add `scores` from sample JSON. +- Transcripts: Set row group size to 25 (specify as rows not bytes). +- Transcripts: Address DuckDB 1.5 compatibility issue w/ mixed type CASE expressions. +- Transcripts: Switch over to async ZIP modules (`async_zip`, `zip_common`, `compression`, `compression_transcoding`, `async_bytes_reader`) that have migrated to `inspect_ai`. +- Transcripts: Remove parquet encryption (not used + issues w/ DuckDB 1.5). +- Transcripts: Ensure that all documented schema columns exist when running transcript queries. +- Observe: Prevent transcript index staleness/warning from occurring when running parallel observe contexts. +- Scout View: Properly sort scanner results using the value type. +- Scout View: Enable minification and caching of view static assets. +- Scout View: Fix issue rendering transcript events when showing the validation panel. +- Scout View: Add 'None' option to column chooser to unselect all columns. +- Bugfix: Fix early-exit bug the failed to unthin `sample_metadata` + ## 0.4.19 (17 February 2026) - LLM Scanner: Store model stop_reason result metadata. diff --git a/README.md b/README.md index e16da4562..9225681c0 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,11 @@ make check make test ``` +### Frontend development (TypeScript) + +The web UI lives in a git submodule and uses Git LFS for binary assets. **These steps are only needed if you plan to work on the TypeScript/React frontend** — Python-only contributors can skip this entirely. + +1. [Install Git LFS](https://git-lfs.com/) and run `git lfs install` +2. Initialize the submodule and install dependencies — see the [one-time setup guide](src/inspect_scout/_view/ts-mono/docs/submodule-guide.md#one-time-setup) + diff --git a/design/claude-code-events.md b/design/claude-code-events.md new file mode 100644 index 000000000..6be665304 --- /dev/null +++ b/design/claude-code-events.md @@ -0,0 +1,193 @@ +# Plan: Bridge ModelEvent Replacement via JSONL Streaming + +## Context + +When running Claude Code as an agent inside inspect_swe, two parallel streams of ModelEvents exist: + +1. **JSONL-derived** (from Claude Code's stdout) — thin ModelEvents but with full agent hierarchy context (spans, tool events, subagent nesting via Task tools) +2. **Bridge-derived** (from inspect_ai's `model.generate()`) — rich ModelEvents with full `ModelCall` data, proper tool definitions, and config, but no agent parenting context + +The goal is to **replace** JSONL-derived ModelEvents with their bridge counterparts while preserving the structural context (spans, tool events, agent hierarchy) that only the JSONL stream provides. This gives us the best of both worlds: rich model call data with correct agent parenting. + +## Key Design Decisions (Resolved) + +### Join Key +The `ChatMessageAssistant.id` (a shortuuid auto-generated in inspect_ai) flows through the entire pipeline: +1. Created in `model_output_from_message()` → `ChatMessageAssistant(...)` → `model_post_init` assigns `self.id = uuid()` +2. Bridge returns it as `Message.id` to Claude Code +3. Claude Code writes it to JSONL as `AssistantMessage.id` + +No changes needed in `anthropic.py` — the ID already flows correctly. + +### Streaming API +`exec_remote(stream=True)` returns an `ExecRemoteProcess` (async iterable, NOT a context manager). It yields: +- `Stdout(data: str)` — raw chunks (not lines) +- `Stderr(data: str)` — raw chunks +- `Completed(exit_code: int)` — process finished + +Since stdout delivers **chunks not lines**, we need a simple line buffer: accumulate chunks, split on `\n`, yield complete lines, keep the remainder. + +Cleanup: call `await proc.kill()` explicitly. Cancellation auto-kills. + +### Subagent Handling +In the streaming/bridge scenario, subagent events arrive **inline** in the same stdout JSONL stream, distinguished by `isSidechain=True` and `sessionId`. The existing `events.py` has a FIFO queue mechanism that buffers and correlates subagent events to their parent Task tool calls. `client.py` (file-based agent loading) is NOT needed for the streaming case. + +### Event Ordering +Bridge ModelEvents always arrive before their JSONL counterparts (bridge processes the API response, returns to Claude Code, then Claude Code writes to JSONL). No buffering strategy needed — the lookup dict will always be populated in time. + +### Fields to Copy During Replacement +Only two fields need copying from the JSONL event to the bridge event: +- `span_id` — JSONL has correct agent hierarchy; bridge has generic context +- `timestamp` — preserves when the event occurred in the JSONL timeline + +Everything else (output, call, config, tools, model, uuid, etc.) stays from the bridge event. + +### Dependency Direction +inspect_scout → inspect_swe is a clean unidirectional dependency. Neither package currently depends on the other. No circular dependency risk. After inspect_swe changes are complete and released, inspect_scout will add inspect_swe as a dependency. + +## Approach + +### Part 1: Preserve message ID in `to_model_event()` ✓ + +**Commit:** `6f8be11a` — set assistant message id from underlying jsonl event + +Added `id=event.message.id or None` to `ChatMessageAssistant(...)` in `to_model_event()`. The `or None` guard normalizes empty strings so `model_post_init` still auto-generates a uuid. Two tests added: ID preservation and empty-string normalization. + +### Part 2: Copy `claude_code_events()` pipeline to inspect_swe + +Copy 7 files from `inspect_scout/src/inspect_scout/sources/_claude_code/` to `inspect_swe/src/inspect_swe/_claude_code/`: + +| Source file | Purpose | +|---|---| +| `models.py` | Pydantic models for JSONL format | +| `detection.py` | Event type detection & filtering | +| `extraction.py` | Message/content extraction | +| `tree.py` | Event tree reconstruction | +| `util.py` | Timestamp parsing | +| `toolview.py` | Tool markdown rendering | +| `events.py` | Core conversion pipeline | + +For `events.py`: copy the conversion functions (`claude_code_events`, `_EventProcessor`, `to_model_event`, etc.) but NOT `transcripts.py`-level orchestration. + +**NOT copied:** `transcripts.py`, `client.py` — these stay in inspect_scout. + +The streaming path in `events.py` already handles inline subagent events via `isSidechain`/`sessionId` buffering — no `client.py` needed. Make `_load_agent_events()` work without file-based loading (the streaming buffer path is already the preferred code path; just ensure the file-based fallback doesn't error when `project_dir` is `None`). + +### Part 3: Streaming exec + JSONL line buffering + +**File:** `inspect_swe/src/inspect_swe/_claude_code/claude_code.py` + +Change from awaitable exec to streaming exec: + +```python +# Before: +result = await sbox.exec_remote(cmd=..., stream=False) +debug_output.append(result.stdout) + +# After: +proc = await sbox.exec_remote(cmd=...) # stream=True is default +try: + line_buffer = "" + async for event in proc: + match event: + case ExecRemoteEvent.Stdout(data=data): + line_buffer += data + while "\n" in line_buffer: + line, line_buffer = line_buffer.split("\n", 1) + yield line # complete JSONL line + case ExecRemoteEvent.Stderr(data=data): + debug_output.append(data) + case ExecRemoteEvent.Completed(exit_code=code): + # handle remaining buffer + exit code +finally: + await proc.kill() +``` + +The retry/attempts loop needs to work with this streaming approach — each attempt produces a streaming process. + +### Part 4: Bridge ModelEvent collection and replacement + +**Collection:** Subscribe to the transcript to intercept completed bridge ModelEvents, indexed by `ChatMessageAssistant.id`: + +```python +bridge_model_events: dict[str, ModelEvent] = {} + +def on_event(event: Event) -> None: + if isinstance(event, ModelEvent) and event.pending is None: + msg_id = event.output.choices[0].message.id if event.output.choices else None + if msg_id: + bridge_model_events[msg_id] = event + +transcript()._subscribe(on_event) +``` + +**Replacement:** Wrap the `claude_code_events()` output to substitute bridge events: + +```python +async for event in claude_code_events(jsonl_lines): + if isinstance(event, ModelEvent): + msg_id = event.output.choices[0].message.id if event.output.choices else None + if msg_id and msg_id in bridge_model_events: + bridge_event = bridge_model_events.pop(msg_id) + bridge_event.span_id = event.span_id + bridge_event.timestamp = event.timestamp + yield bridge_event + continue + yield event +``` + +### Part 5: Wire it together + +The overall flow in `claude_code.py`: + +1. Set up bridge as before (`sandbox_agent_bridge`) +2. Subscribe to transcript to collect bridge ModelEvents +3. Start Claude Code with streaming exec +4. Line-buffer stdout chunks into complete JSONL lines +5. Parse JSONL lines → feed into `claude_code_events()` +6. For each yielded event: + - If ModelEvent: look up bridge counterpart by message ID, replace if found + - Otherwise: yield as-is (SpanBeginEvent, SpanEndEvent, ToolEvent, etc.) +7. All events go into the transcript — rich model data with correct agent hierarchy + +### Part 6: Update inspect_scout to import from inspect_swe + +After inspect_swe is released with the new `_claude_code/` modules: + +1. Add `inspect_swe` as a dependency in inspect_scout's `pyproject.toml` +2. Update `inspect_scout.sources._claude_code.transcripts` to import `claude_code_events` and related functions from `inspect_swe._claude_code.events` +3. Remove the local copies of the 7 moved files from inspect_scout +4. Run all inspect_scout tests to verify + +## Key Files + +| File | Repo | Action | +|---|---|---| +| `src/inspect_swe/_claude_code/claude_code.py` | inspect_swe | Modify: streaming exec, line buffering, event replacement | +| `src/inspect_swe/_claude_code/events.py` | inspect_swe | New: copied from inspect_scout | +| `src/inspect_swe/_claude_code/models.py` | inspect_swe | New: copied from inspect_scout | +| `src/inspect_swe/_claude_code/detection.py` | inspect_swe | New: copied from inspect_scout | +| `src/inspect_swe/_claude_code/extraction.py` | inspect_swe | New: copied from inspect_scout | +| `src/inspect_swe/_claude_code/tree.py` | inspect_swe | New: copied from inspect_scout | +| `src/inspect_swe/_claude_code/util.py` | inspect_swe | New: copied from inspect_scout | +| `src/inspect_swe/_claude_code/toolview.py` | inspect_swe | New: copied from inspect_scout | +| `src/inspect_scout/sources/_claude_code/events.py` | inspect_scout | Part 1: add message ID; Part 6: re-import from inspect_swe | +| `src/inspect_scout/sources/_claude_code/transcripts.py` | inspect_scout | Part 6: update imports | +| `pyproject.toml` | inspect_scout | Part 6: add inspect_swe dependency | + +## Verification + +1. **Part 1:** Unit test — verify `event.message.id` flows through `to_model_event()` to `ModelEvent.output.choices[0].message.id` +2. **Part 2:** Run copied module tests in inspect_swe (adapt from inspect_scout test suite) +3. **Parts 3-5:** Integration test — run a Claude Code eval through inspect_swe, verify: + - ModelEvents have `call` data (from bridge) + - Span hierarchy is correct (from JSONL) + - Agent nesting is preserved for Task tool calls +4. **Part 6:** Run full `pytest` in inspect_scout after import changes + +## Working Guidelines + +1. **One part at a time.** Implement, test, and verify each part before moving to the next. +2. **Plan each part.** Each part should have its own sub-plan discussed and approved before coding. +3. **Review before commit.** After tests pass, pause and review the code together before committing. Do not auto-commit. +4. **Full tests at each step.** Every phase produces implementation and tests. diff --git a/design/documentation-plan.md b/design/documentation-plan.md new file mode 100644 index 000000000..6866080cd --- /dev/null +++ b/design/documentation-plan.md @@ -0,0 +1,253 @@ +# Documentation Plan: Scanner Articles + +## Status + +The following scanner documentation is already complete and does not need further work: + +- **`scanners.qmd`** — Overview, using scanners, includes for LLM/Grep/Custom scanner summaries, scan jobs, learning more links +- **`llm_scanner.qmd`** — Basic usage, answer types, prompt templates, scanner results, message filtering, structured answers, value to float, dynamic questions, context windows, scanning timelines, parameter reference. **TODO:** Add callout near top (e.g. in Overview) pointing to Scanner Tools article for users who need custom prompting logic beyond what `llm_scanner()` offers. +- **`grep_scanner.qmd`** — Basic usage, pattern types, options, scanner results, searching events, examples + +The following articles exist and are also complete: + +- **`db_importing.qmd`** — Claude Code source, well documented + +## Uncovered Topics + +The topics below need new documentation. They are currently either undocumented or live in `custom_scanner.qmd` which is being reorganized into new articles. + +### Transcripts & Timelines + +Data model and content access — what scanners operate on. + +| Topic | Source | Notes | +|-------|--------|-------| +| **Transcript fields** | `custom_scanner.qmd` | Field table via `_transcript_fields.md` include. Currently missing the `timelines` field. | +| **Content filtering** | `custom_scanner.qmd` | How `messages`, `events`, and `timeline` filters on `@scanner` control what gets loaded. Examples of `messages="all"`, `events=["model", "tool"]`, `messages=["assistant"]`. | +| **`Transcript.timelines`** | New | New field on `Transcript`. Missing from `_transcript_fields.md`. | +| **Timeline data model** | New | `Timeline`, `TimelineSpan`, `TimelineEvent`, `TimelineBranch` — the node types, their fields, span types, branches, outline, token counts. Currently only stub listings in `reference/transcript.qmd`. | +| **`build_timeline(events)`** | New | Converts flat event list into hierarchical timeline. No narrative docs exist. | +| **`filter_timeline(timeline, predicate)`** | New | Prune a timeline by span predicate. No narrative docs exist. | +| **`timeline.render()`** | New | ASCII swimlane diagram of a timeline. Undocumented. | +| **Presenting messages** | `custom_scanner.qmd` | `messages_as_str()` function, `MessagesPreprocessor` usage for custom scanners. | +| **Message extraction pipeline** | New | `transcript_messages`, `segment_messages`, `span_messages`, `timeline_messages`, `MessagesSegment`, `TimelineMessages` — the full pipeline for extracting and segmenting messages from transcripts and timelines. | + +### Custom Scanners + +How to write scanners — the authoring guide. + +| Topic | Source | Notes | +|-------|--------|-------| +| **Scanner basics** | `custom_scanner.qmd` | What a `Scanner` is, the function signature, `Result` type, `value`/`answer`/`explanation`/`references` fields. | +| **`@scanner` decorator** | `custom_scanner.qmd` | Decorator parameters: `messages`, `events`, `timeline`, `name`, `version`, `metrics`, `loader`. The `timeline` and `version` params are new and not yet documented in prose. | +| **Input types** | `custom_scanner.qmd` | `Transcript`, `Event`, `ChatMessage`, `list[Event]`, `list[ChatMessage]`. | +| **Input filtering** | `custom_scanner.qmd` | Performance principle — only requested data is deserialized. Default is no filters (scanner won't be called without them). | +| **Transcript scanners** | `custom_scanner.qmd` | Most common type. Includes the "confusion" example with `get_model().generate()`. | +| **Event scanners** | `custom_scanner.qmd` | `Scanner[ModelEvent]`, `Scanner[ModelEvent | ToolEvent]`, auto-inference of event filters from type annotations. | +| **Message scanners** | `custom_scanner.qmd` | `Scanner[ChatMessageTool]`, `Scanner[ChatMessageUser | ChatMessageAssistant]`, auto-inference of message filters. | +| **Timeline scanners** | New | `@scanner(timeline=True)`, `Scanner[Timeline]` — auto-inference from type annotation. Covered implicitly in `llm_scanner.qmd` examples but not in custom scanner context. | +| **Multiple results** | `custom_scanner.qmd` | Returning `list[Result]` with `label` field, interaction with results data frame and validation. | +| **Custom loaders** | `custom_scanner.qmd` | `@loader` decorator, yielding custom content (e.g., message pairs), `loader=` parameter on `@scanner`. | + +### Scanner Integration + +Packaging, distribution, and reuse with Inspect. + +| Topic | Source | Notes | +|-------|--------|-------| +| **Packaging** | `custom_scanner.qmd` | Python packages, `_registry.py`, setuptools entry points (`inspect_ai`), `scout scan myscanners/reward_hacking`. | +| **Scanners as scorers** | `custom_scanner.qmd` | Using scanners in Inspect `Task`, `inspect score` CLI, `as_scorer()` function. | +| **Scanner metrics** | `custom_scanner.qmd` | `metrics=` on `@scanner`, built-in Inspect metrics, custom metrics. Via `_scanner_metrics.md` include. | +| **Result set metrics** | `custom_scanner.qmd` | Dict-based metrics for multi-label scanners, glob (`*`) shorthand. | + +### Scanner Tools + +Lower-level building blocks for advanced custom scanners. + +| Topic | Source | Notes | +|-------|--------|-------| +| **`generate_answer()`** | New | Standalone LLM generation that returns a `Result`. Undocumented. | +| **`parse_answer()`** | New | Pure parsing of model output into a `Result`, no LLM call. Undocumented. | +| **`message_numbering()`** | New | Creates `[M1]`, `[M2]` counter pair for cross-segment consistency. Undocumented. | +| **`scan_segments()`** | New | Concurrent segment scanning with ordered results. Undocumented. | +| **`AnswerSpec`** | New | Type alias unifying all answer specifications. Undocumented. | +| **`ResultReducer`** | New | `mean/median/mode/max/min/any/union/last` static reducers, `ResultReducer.llm()` factory. Documented in `llm_scanner.qmd` context windows section but not as standalone reference. | + +### Other Gaps + +| Topic | Priority | Notes | +|-------|----------|-------| +| **`scout import` CLI command** | High | Entirely undocumented. `scout import [options]`, flags: `--limit`, `--from`, `--to`, `-P key=value`, `--dry-run`, `--overwrite`, `--sources`. | +| **CLI reference stubs** | Low | Pre-existing gap — all empty. | + +## Proposed Article Structure + +Based on the Option B (progressive disclosure) approach discussed: + +1. **Custom Scanners** — Entry point for scanner authoring. Scanner basics, `@scanner` decorator, `Result`, input types and filtering, transcript/event/message/timeline scanners, multiple results, custom loaders, packaging, scanners as scorers, metrics. + +2. **Scanner Tools** — Power tools for advanced scanner development that apply to all custom scanners. `generate_answer`, `parse_answer`, `message_numbering`, `scan_segments`, `ResultReducer`, message extraction pipeline. + +3. **Timelines** — Specialized: understanding hierarchical agent execution traces. Timeline data model (`Timeline`, `TimelineSpan`, `TimelineEvent`, `TimelineBranch`), `build_timeline`, `filter_timeline`, `render()`. + +--- + +## Article Outlines + +### Article 1: Custom Scanners (`custom_scanners.qmd`) + +*Audience:* Someone who has used `llm_scanner()` or `grep_scanner()` and now needs more control. + +*Sequence:* Concepts first (what is a scanner, what can it target), then progressively more advanced patterns. + +#### Overview +- What custom scanners are and when you need them (vs. `llm_scanner`/`grep_scanner`) +- Scanner as a function: takes `ScannerInput`, returns `Result` +- Link to LLM Scanner and Grep Scanner for the common cases + +#### Scanner Basics +- The `@scanner` decorator and its role +- The inner function pattern (factory returns scan function) +- `Result` type: `value`, `answer`, `explanation`, `references`, `label` +- Simple example: the "confusion" scanner with `get_model().generate()` +- Comparison: same scanner implemented with `llm_scanner()` (show both, motivate when custom is needed) + +#### Input Types & Filtering +- The performance principle: only requested data is deserialized +- **Important:** default is no filters — scanner won't be called without `messages` and/or `events` +- `@scanner` filter parameters: `messages`, `events`, `timeline` +- Filter values: `"all"`, specific types (`["assistant"]`, `["model", "tool"]`) + +#### Transcript Scanners +- Most common type (`Scanner[Transcript]`) +- Accessing `transcript.messages`, `transcript.events`, `transcript.metadata` +- Presenting messages and extracting references with `message_numbering()` + - Returns a `(messages_as_str, extract_references)` pair with shared numbering + - `messages_as_str()` produces numbered text (`[M1]`, `[M2]`, ...) for LLM prompts + - `extract_references()` resolves model citations back to message IDs for `Result.references` + - Update "confusion" example to use `message_numbering()` instead of bare `messages_as_str()` + +#### Event Scanners +- `Scanner[ModelEvent]`, `Scanner[ModelEvent | ToolEvent]` +- Auto-inference: type annotation implies event filter (no need for `events=` on decorator) +- Called once per matching event in the transcript + +#### Message Scanners +- `Scanner[ChatMessageTool]`, `Scanner[ChatMessageUser | ChatMessageAssistant]` +- Auto-inference from type annotation +- Called once per matching message + +#### Timeline Scanners +- `@scanner(timeline=True)` or `Scanner[Timeline]` type annotation +- How timeline scanning differs: each span scanned independently +- Brief conceptual intro (link to Timelines article for the full data model) + +#### Multiple Results +- Returning `list[Result]` with `label` field +- Each result yields its own row in the results data frame +- Interaction with result set validation (link) + +#### Custom Loaders +- When built-in iteration isn't enough (e.g., message pairs) +- `@loader` decorator with `messages`/`events`/`timeline` filters +- Yielding custom content via `AsyncIterator` +- Using `loader=` parameter on `@scanner` + +#### Packaging +- Including scanners in Python packages +- `_registry.py` and setuptools entry points +- `scout scan myscanners/reward_hacking` +- Tabset: Setuptools / uv / Poetry + +#### Scanners as Scorers +- Using scanners directly in Inspect `Task(scorer=...)` +- `inspect score` CLI +- `as_scorer()` for explicit conversion +- Metrics: default `mean()`/`stderr()`, custom via `@scanner(metrics=...)` +- Result set metrics for multi-label scanners (dict-based, glob shorthand) + +--- + +### Article 2: Scanner Tools (`scanner_tools.qmd`) + +*Audience:* Advanced users building custom scanners that need the same answer parsing, generation, segmentation, and message extraction infrastructure that `llm_scanner()` uses internally. + +*Sequence:* Start with the highest-value tools (answer generation/parsing), then message presentation and extraction, then segmentation and reduction. + +#### Overview +- The scanner tools are the building blocks used internally by `llm_scanner()` +- Use these when `llm_scanner()` is too opinionated but you want its infrastructure +- Motivating example: show a skeleton custom scanner that decomposes `llm_scanner()` into its parts: + 1. `message_numbering()` — format messages with `[M1]`/`[M2]` numbering and extract references + 2. `segment_messages()` — split to fit context windows + 3. **Custom prompt construction** ← your logic here + 4. `generate_answer()` / `parse_answer()` — get a parsed `Result` + 5. `ResultReducer` — combine multi-segment results +- This skeleton shows where each tool fits and where users inject their own prompting logic + +#### Answer Generation +- `generate_answer(prompt, answer, ...)` — send a prompt to an LLM and get back a parsed `Result` +- Supports all `AnswerSpec` types (boolean, numeric, string, labels, structured) +- Handles retries for refusals and schema validation + +#### Answer Parsing +- `parse_answer(output, answer, ...)` — parse model output into a `Result` without making an LLM call +- Useful for custom generation flows where you handle the model call yourself + +#### Answer Types Reference +- `AnswerSpec` type alias — unifies all answer specifications +- `AnswerMultiLabel` — multi-classification +- `AnswerStructured` — tool-based structured output with `answer_tool`, `answer_prompt`, `answer_format`, `max_attempts` + +#### Presenting Messages +- `MessagesPreprocessor` options: `exclude_system`, `exclude_reasoning`, `exclude_tool_usage`, `transform` +- Cross-segment numbering: `message_numbering()` maintains consistent `[M1]`...`[Mn]` IDs across multiple segments (counter auto-increments globally so segment 2 continues where segment 1 left off) +- `extract_references()` resolves citations from any prior `messages_as_str()` call within the numbering scope + +#### Message Extraction Pipeline +- `transcript_messages(transcript, ...)` — unified entry point; auto-selects strategy (timelines → events → messages) +- `span_messages(source, ...)` — extract from a span/event list with compaction handling +- `segment_messages(source, ...)` — segment messages to fit context windows +- `timeline_messages(timeline, ...)` — walk span tree yielding `TimelineMessages` per non-utility span +- `MessagesSegment`, `TimelineMessages` — return types + +#### Segment Scanning +- `scan_segments()` — concurrent scanning of multiple message segments with ordered results +- Use case: scanning long transcripts in parallel chunks + +#### Result Reduction +- `ResultReducer` — combining results from multiple segments +- Static reducers: `mean`, `median`, `mode`, `max`, `min`, `any`, `union`, `last` +- `ResultReducer.llm(model, prompt)` — LLM-based synthesis of multi-segment results + +--- + +### Article 3: Timelines (`timelines.qmd`) + +*Audience:* Someone working with multi-agent or complex agent transcripts who needs to understand the hierarchical structure of execution traces. + +*Sequence:* Conceptual intro, then the data model, then construction/filtering/rendering. + +#### Overview +- What a timeline represents: hierarchical tree of spans from agent execution +- Why timelines matter: flat message lists lose agent structure, tool boundaries, and branching +- Built automatically from transcript events +- Available on `Transcript` via the `timelines` field (requires `timeline=True` on `@scanner`) + +#### Timeline Data Model +- `Timeline` — root container +- `TimelineSpan` — agent/tool/scorer invocation; fields: `name`, `span_type`, `content`, `branches`, `outline`, token counts +- `TimelineEvent` — individual event within a span +- `TimelineBranch` — re-rolled attempts + +#### Building Timelines +- `build_timeline(events)` — converts flat events to hierarchical tree +- What it detects: agent hierarchies, conversation threads, branches, utility agents + +#### Filtering Timelines +- `filter_timeline(timeline, predicate)` — prune by span predicate +- Utility spans: what they are, why they're excluded by default + +#### Rendering Timelines +- `timeline.render()` — ASCII swimlane diagram +- Example output diff --git a/design/scout-import.md b/design/scout-import.md new file mode 100644 index 000000000..e85a5374d --- /dev/null +++ b/design/scout-import.md @@ -0,0 +1,159 @@ +# Scout Import + +## Overview + +Add a `scout import` CLI command that imports transcripts from any registered source into a local transcript database. Sources are the async generator functions exported from `inspect_scout.sources` (`claude_code`, `logfire`, `langsmith`, `phoenix`). + +## Motivation + +Currently, importing transcripts requires writing a Python script (see `examples/import_cc/import_cc.py`). A CLI command makes this accessible without code, supports quick exploration, and provides a consistent interface across all sources. + +## Usage + +```bash +# Basic import +scout import claude_code +scout import logfire -P project=my-project + +# With common filters +scout import claude_code --limit 10 +scout import langsmith --from 2025-01-01 --to 2025-02-01 + +# Source-specific parameters via -P +scout import claude_code -P session_id=abc123 +scout import phoenix -P tags='[tag1, tag2]' -P metadata='{key: value}' + +# Output control +scout import claude_code --transcripts ./my-transcripts +scout import claude_code -T ./my-transcripts + +# Discovery +scout import --sources + +# Dry run +scout import claude_code --dry-run +scout import claude_code --dry-run --limit 5 +``` + +## CLI Parameters + +| Parameter | Short | Description | Default | +|-----------|-------|-------------|---------| +| `SOURCE` | | Positional: source function name | required (except with `--sources`) | +| `--transcripts` | `-T` | Transcripts database directory | `./transcripts` | +| `--limit` | | Max transcripts to import | None (all) | +| `--from` | | Only transcripts on/after this time (ISO 8601) | None | +| `--to` | | Only transcripts before this time (ISO 8601) | None | +| `-P` | | Source parameter as `name=value` (repeatable) | None | +| `--sources` | | List available sources and their parameters | False | +| `--dry-run` | | Fetch and display summary without writing | False | + +The import command always prints the `scout view` command after a successful import. + +## Parameter Parsing (`-P`) + +Values passed via `-P name=value` are parsed with a YAML parser to support rich types: + +| Input | Parsed as | Python type | +|-------|-----------|-------------| +| `-P limit=10` | `10` | `int` | +| `-P project=my-proj` | `"my-proj"` | `str` | +| `-P tags='[a, b, c]'` | `["a", "b", "c"]` | `list[str]` | +| `-P metadata='{k: v}'` | `{"k": "v"}` | `dict[str, str]` | +| `-P trace_id=abc123` | `"abc123"` | `str` | + +**Special handling for `datetime` parameters**: If a source function's type annotation indicates `datetime`, the string value is parsed with `datetime.fromisoformat()` rather than YAML. This allows natural ISO 8601 input like `-P from_time=2025-01-01` or `-P from_time=2025-01-01T10:00:00`. + +**Promoted parameters**: `limit`, `from_time`, and `to_time` can be specified either as top-level CLI params (`--limit`, `--from`, `--to`) or via `-P`. The top-level params take precedence if both are specified. + +## Source Discovery + +Sources are discovered dynamically from `inspect_scout.sources.__all__`. For each name in `__all__`, we import the function and use `inspect.signature()` to get its parameters for validation and `--sources` display. + +### `--sources` Output + +``` +Available sources: + + claude_code + path str + session_id str + from_time datetime + to_time datetime + limit int + + logfire + project str + from_time datetime + to_time datetime + filter str + trace_id str + limit int + read_token str + + ... +``` + +Shows parameter names and types, extracted via `inspect.signature()`. + +## Progress Reporting + +The current progress display in `_insert_from_transcripts` shows a spinner with transcript_id and count. We should improve this to provide a richer import experience: + +**During import** — Enhanced progress showing: +- Source name and parameters being used +- Running count of transcripts imported +- Elapsed time + +**After import** — Summary showing: +- Total transcripts imported (and skipped as duplicates) +- Total time elapsed +- Database location +- The `scout view` command to run + +**Dry run output** — Table showing: +- Transcript ID, date, model, agent, message count, token count +- Summary count at the bottom + +## Existing Transcripts Directory + +If the `--transcripts` directory already exists, prompt the user: + +``` +Transcripts directory './transcripts' already exists. +Add transcripts to it? (existing transcripts won't be re-imported) [y/N] +``` + +If the user confirms, proceed with the import — the database's built-in deduplication by `transcript_id` ensures no duplicates. If declined, exit without importing. + +This prompt is skipped when the directory does not exist (fresh import). + +## Error Handling + +- If a source function raises during iteration, report the error and keep any transcripts already written (the database batching already handles partial writes). +- If an unknown source name is given, show available sources and exit. +- If an unknown `-P` parameter is given for a source, show that source's valid parameters and exit. +- Validate parameter types before calling the source function (e.g., confirm `limit` parses as int). + +## Implementation + +### New Files + +- `src/inspect_scout/_cli/import_command.py` — The Click command implementation + +### Modified Files + +- `src/inspect_scout/_cli/main.py` — Register the new command + +### Key Implementation Details + +1. **Command registration** follows the existing pattern in `main.py`: + ```python + scout.add_command(import_command) + ``` + +2. **Source function invocation** — Use `inspect.signature()` to bind parsed parameters, coerce types based on annotations, then call the async generator. + +3. **Database write** — Use `transcripts_db(to_dir)` context manager and `db.insert()`, matching the pattern in `examples/import_cc/import_cc.py`. + +4. **Async execution** — Wrap the async import in `asyncio.run()` at the CLI boundary, consistent with other commands. diff --git a/design/timeline-context.md b/design/timeline-context.md new file mode 100644 index 000000000..8220ec5dd --- /dev/null +++ b/design/timeline-context.md @@ -0,0 +1,1393 @@ +# Timeline UI Spec + + +## 1. Sequential Sub-Agents + +A coding agent that explores the codebase, plans an approach, then builds the solution. The Transcript row spans the full timeline and is selectable (to show the whole trajectory). Solver agents and Scoring are indented beneath it. + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ Transcript 48.5k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Transcript│██████████████████████████████████████████████████████│ │ +│ Explore │ ███████████ │ 8.1k │ +│ Plan │ ████████ │ 5.3k │ +│ Build │ ████████████████████████████ │ 31.8k │ +│ Scoring │ ███│ 3.2k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +**Notes:** +- Init phase is hidden by default +- Small gaps between agents represent the orchestrator's own model calls + +## 2. Iterative Sub-Agents (Multiple Spans) + +An agent that iterates between exploring and planning before building. When a named sub-agent has multiple spans, they appear on the same swimlane row. Token counts show the total across all spans. + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ Transcript 61.5k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Transcript│██████████████████████████████████████████████████████│ │ +│ Explore │ ███████ ██████ │ 14.5k │ +│ Plan │ ██████ █████ │ 9.2k │ +│ Build │ █████████████████████ │ 34.6k │ +│ Scoring │ ███│ 3.2k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +**Notes:** +- Token counts are aggregated across all spans for each agent + +## 3. Agent Navigation + +Agents can nest to arbitrary depth. Rather than showing all levels at once (which exhausts horizontal space), the timeline uses drill-down navigation. Clicking a sub-agent zooms in: its timeline rescales to fill the full width, revealing its children. A breadcrumb and back arrow provide navigation. + +### Top Level + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ Transcript 48.5k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Transcript│██████████████████████████████████████████████████████│ │ +│ Explore │ ███████████ │ 8.1k │ +│ Plan │ ████████ │ 5.3k │ +│ Build │ ████████████████████████████ | 31.8k │ +│ Scoring │ ███│ 3.2k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +Clicking Build zooms into it: + +### Zoomed into Build + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ ← Transcript › Build 31.8k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Build │██████████████████████████████████████████████████████│ │ +│ Code │ ██████████████████████ │ 15.2k │ +│ Test │ ████████████████ │ 10.4k │ +│ Fix │ ███████████████│ 6.2k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +**Notes:** +- `Escape` key also returns to parent level +- Each breadcrumb segment is clickable + +### Zoomed into Test + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ ← Transcript › Build › Test 10.4k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Test │██████████████████████████████████████████████████████│ │ +│ Generate│ ████████████████ │ 5.8k │ +│ Run │ ██████████ │ 0.4k │ +│ Evaluate│ ██████████████████████████│ 4.2k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +**Notes:** +- Clicking any breadcrumb segment jumps directly to that level +- No limit on nesting depth + +## 4. Parallel Sub-Agents + +Multiple Explore agents run in parallel, followed by sequential Plan and Build. Parallel agents of the same type are shown as a single row with a `(3)` count badge. The envelope bar spans from the earliest start to the latest end. Clicking the group row drills down (same pattern as section 3) to reveal individual instances. + +### Top Level + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ Transcript 60.4k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Transcript │██████████████████████████████████████████████████│ │ +│ Explore (3) │ █████████████ │ 24.3k │ +│ Plan │ ███████ │ 5.3k │ +│ Build │ ████████████████████████ │ 27.6k │ +│ Scoring │ ██│ 3.2k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +Clicking Explore (3) drills into the parallel group: + +### Zoomed into Explore (3) + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ ← Transcript › Explore (3) 24.3k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Explore (3)│██████████████████████████████████████████████████████│ │ +│ Explore 1 │ ██████████████████████████████████████████ │ 8.1k │ +│ Explore 2 │ ███████████████████████████████████████████████████│ 9.4k │ +│ Explore 3 │ ████████████████████████████████████ │ 6.8k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +**Notes:** +- Token count on the group row is the sum of all instances +- At full width, timing differences between instances are much easier to see + +## 5. Inline Markers + +Errors and compaction events are shown as subtle inline markers within timeline bars. These provide positional context — you can see *where* in the execution something happened without leaving the timeline view. + +### Marker Characters + +- `▲` — Error (tool error, model error, etc.) +- `┊` — Compaction (context window was compressed at this point) + +### Example + +A single Transcript bar showing two errors and one compaction: + +``` +│ Transcript│██████████████▲██████████┊██████████▲████████████████│ │ +``` +{{< pagebreak >}} + +Multiple agents with markers on different rows: + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ Transcript 48.5k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Transcript│██████████████▲██████████┊██████████▲█████████████████│ │ +│ Explore │ ███████████ │ 8.1k │ +│ Plan │ ████████ │ 5.3k │ +│ Build │ ████████▲███████┊██▲████████████│ 31.8k │ +│ Scoring │ ███│ 3.2k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +**Notes:** +- The Transcript row aggregates all markers from its children +- Markers replace a single `█` character — they don't widen the bar + +## 6. Selection and Navigation + +Single click selects a row (driving the content panel below). Drill-down is a separate action via a `›` chevron attached to each bar, or double-click. This preserves browsing — you can click between sibling agents to compare their events without navigating in and out. + +### Selected State (Sequential) + +Build is selected. Chevrons on each bar indicate drillable agents: + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ Transcript 48.5k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Transcript│██████████████████████████████████████████████████████│ │ +│ Explore │ ██████████› │ 8.1k │ +│ Plan │ ███████› │ 5.3k │ +│▸ Build │ ███████████████████████████████›│ 31.8k │ +│ Scoring │ ███│ 3.2k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### Selected State (Iterative) + +Each span gets its own chevron — you can drill into a specific invocation: + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ Transcript 61.5k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Transcript│██████████████████████████████████████████████████████│ │ +│▸ Explore │ ██████› █████› │ 14.5k │ +│ Plan │ █████› ████› │ 9.2k │ +│ Build │ ██████████████████████› │ 34.6k │ +│ Scoring │ ███│ 3.2k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### Interactions + +| Action | Effect | +|--------|--------| +| Click row | Select — highlights row, shows its events in content panel | +| Click `›` on bar | Drill down into that specific agent span | +| Double-click bar | Drill down (shortcut) | +| `Enter` | Drill down into selected row | +| `Escape` | Zoom out one level (or deselect if at top) | +| `↑` / `↓` | Move selection between rows | + +**Notes:** +- Leaf agents (Scoring) and the Transcript row have no chevron +- In the iterative case, clicking `›` on a specific span drills into just that invocation +- Selection changes only update the content panel — the timeline stays stable + +## 7. Flat Transcript (No Sub-Agents) + +A simple eval with no agent hierarchy — just model calls and tool calls. The timeline shows a single Transcript bar. Inline markers provide landmarks for navigating the execution. + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ Transcript 12.4k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Transcript│████████████████████▲█████████┊███████████████████████│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +**Notes:** +- Compact timeline leaves maximum vertical space for the content panel + +## 8. Timeline Height + +The timeline panel height adapts to content, up to a cap of 6 rows. A resizable divider between the timeline and content panel lets users override the default. + +### Behavior + +- **Adaptive**: timeline grows to fit its rows — 2 agents = 2 rows, no wasted space +- **Cap at 6 rows**: beyond 6, the swimlane area scrolls internally +- **Resizable divider**: draggable border between timeline and content panel + +**Notes:** +- When scrolled, the Transcript parent row could optionally stay pinned at the top + +## 9. Content Panel + +The content panel sits below the timeline, separated by the resizable divider (section 8). It shows the selected agent's events and sub-agent launches in chronological order. All events are always expanded. Sub-agent cards appear inline at their launch point and are clickable to drill down. + +### Events and Sub-Agent Cards + +Build is selected, showing model calls, a tool call, and three sub-agent launches: + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ ← Transcript › Build 31.8k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Build │██████████████████████████████████████████████████████│ │ +│ Code │ ██████████████████████ │ 15.2k │ +│ Test │ ████████████████ │ 10.4k │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ◆ MODEL 2.1k │ +│ I'll start by reading the existing codebase to understand the │ +│ architecture before making changes. │ +│ │ +│ ◇ TOOL read_file 0.3s │ +│ path: "src/main.py" │ +│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │ +│ import os │ +│ from app import create_app │ +│ ... │ +│ │ +│ ◆ MODEL 3.4k │ +│ Based on the code, I need to modify the authentication module. │ +│ Let me delegate the implementation... │ +│ │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ Code 15.2k · 12.4s │ │ +│ │ "Implement the authentication changes" │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +│ ◆ MODEL 1.2k │ +│ Code changes complete. Running tests... │ +│ │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ Test 10.4k · 8.2s │ │ +│ │ "Run test suite and verify changes" │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +│ ◆ MODEL 0.6k │ +│ All tests passing. Changes complete. │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +{{< pagebreak >}} +### Parallel Sub-Agents + +Parallel sub-agents are grouped with a `┃` gutter connecting their cards: + +``` +│ │ +│ ◆ MODEL 1.8k │ +│ I'll research this from multiple angles... │ +│ │ +│ ┃ ┌──────────────────────────────────────────────────────────┐ │ +│ ┃ │ Explore 1 8.1k · 6.2s │ │ +│ ┃ │ "Search for API documentation" │ │ +│ ┃ └──────────────────────────────────────────────────────────┘ │ +│ ┃ ┌──────────────────────────────────────────────────────────┐ │ +│ ┃ │ Explore 2 9.4k · 7.8s │ │ +│ ┃ │ "Analyze existing codebase" │ │ +│ ┃ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ◆ MODEL 2.1k │ +│ Research complete. Synthesizing findings... │ +│ │ +``` + +### Errors and Compaction + +A tool error uses the same `▲` marker as the timeline. Compaction appears as a horizontal divider: + +``` +│ │ +│ ◇ TOOL web_search ▲ ERROR 0.3s │ +│ query: "solar panel efficiency" │ +│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │ +│ Error: Rate limit exceeded │ +│ │ +│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ COMPACTION ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │ +│ │ +│ ◆ MODEL 1.5k │ +│ Continuing after context compression... │ +│ │ +``` + +**Notes:** +- Clicking a sub-agent card drills down (same as double-click or `›` in the timeline) +- `▲ ERROR` and compaction dividers correspond to inline markers in the timeline bars +- `◆` / `◇` distinguish model calls from tool calls + +## 10. Utility Agents + +Utility agents (bash checkers, safety validators, etc.) can number in the dozens — one per tool call — and are rarely interesting. They never appear in the timeline. In the content panel, they are hidden by default behind a toggle. + +### Hidden (Default) + +A collapsed toggle indicates their presence: + +``` +│ │ +│ ▸ 12 utility agents hidden │ +│ │ +│ ◆ MODEL 2.1k │ +│ I'll start by reading the codebase... │ +│ │ +│ ◇ TOOL bash 0.8s │ +│ command: "npm test" │ +│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │ +│ Tests passed: 42/42 │ +│ │ +``` + +### Revealed + +Toggling shows utility agents as single-line entries with `⚙`, inline where they ran: + +``` +│ │ +│ ▾ 12 utility agents shown │ +│ │ +│ ◆ MODEL 2.1k │ +│ I'll start by reading the codebase... │ +│ │ +│ ◇ TOOL bash 0.8s │ +│ command: "npm test" │ +│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │ +│ Tests passed: 42/42 │ +│ │ +│ ⚙ bash_checker 0.3k · 0.1s │ +│ ⚙ safety_validator 0.2k · 0.2s │ +│ │ +``` + +**Notes:** +- `⚙` entries are single-line — no box, no task description +- Utility agents are never shown in the timeline, only in the content panel +- The toggle count reflects the selected agent's utility agents, not the whole trace + +## 11. Branches + +Branches represent alternative execution paths forked from a point in an agent's trajectory. They appear when an agent retries, backtracks, or explores multiple strategies. Each branch shares the same events up to the fork point, then diverges. + +### Timeline: Inline Branch Marker + +Branches don't get their own swimlane rows. Instead, a `↳` marker appears inline on the parent agent's bar at each fork point. Clicking the marker opens a popover listing the branches at that point. Selecting a branch from the popover drills down. + +#### Branch Markers on Timeline Bars + +Build has two child agents and a fork point with two branches: + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ ← Transcript › Build 31.8k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Build │████████████↳████████████████████████████████████████████│ │ +│ Code │ ██████████████████████ │15.2k│ +│ Test │ ████████████████ │10.4k│ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +#### Multiple Fork Points + +Each fork point gets its own `↳` marker: + +``` +│ Build │████████████↳████████████↳██████████████████████████████│ │ +``` + +#### Branch Popover + +Clicking a `↳` marker opens a popover listing the branches at that fork point. Each entry shows label, tokens, and duration: + +``` +┌──────────────────────────────────┐ +│ ↳ branch 1 8.7k · 6.1s │ +│ ↳ branch 2 5.1k · 4.3s │ +└──────────────────────────────────┘ +``` + +Clicking a branch entry drills down: + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ ← Transcript › Build › ↳ branch 1 8.7k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ ↳ branch 1 │██████████████████████████████████████████████████████│ │ +│ Refactor │ ████████████████████ │ 5.2k │ +│ Validate │ ██████████████████████████████ │ 3.5k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +**Notes:** +- `↳` markers replace a single `█` character — same pattern as error/compaction markers (section 5) +- The popover is lightweight — just a list, not a full panel +- Keeps the timeline compact; branches don't consume swimlane rows + +### Content Panel: Branch Cards + +Branch cards use dashed borders (`╌`) to distinguish from child agent cards (`─`). They appear inline at the fork point — after the `forkedAt` event — and show label, tokens, duration, and first model output preview. Clicking a branch card drills down. + +#### Branch Cards at Fork Point + +``` +│ │ +│ ◆ MODEL 3.4k │ +│ The current approach isn't working. Let me try a different │ +│ strategy... │ +│ │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ Code 15.2k · 12.4s │ │ +│ │ "Implement the authentication changes" │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ │ +│ ╎ ↳ branch 1 8.7k · 6.1 ╎ │ +│ ╎ "Let me try a recursive approach instead" ╎ │ +│ └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ │ +│ ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ │ +│ ╎ ↳ branch 2 5.1k · 4.3 ╎ │ +│ ╎ "Attempting a greedy algorithm approach" ╎ │ +│ └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ │ +│ │ +│ ◆ MODEL 1.2k │ +│ Tests passing. Changes complete. │ +│ │ +``` + +**Notes:** +- Dashed borders (`╌` / `╎`) visually separate branch cards from child agent cards (`─` / `│`) +- Multiple branches at the same fork point appear consecutively +- Branch cards show the first model output as a preview, same as child agent cards +- Clicking a branch card drills down (same as selecting from the `↳` popover) + +### Naming + +- **Explicit branches**: use the span `name` from the transcript (e.g., `↳ retry`, `↳ backtrack`) +- **Auto-detected branches**: numbered sequentially — `↳ branch 1`, `↳ branch 2`, etc. +- The `↳` prefix is always shown to distinguish branches from child agents + +## 12. Multiple Timelines + +A single transcript can have multiple timeline interpretations. Each timeline is a named view with its own `TimelineSpan` tree — the same event stream parsed with different groupings, filters, or analytical lenses. For example, a default agent-centric timeline alongside a phase-based or domain-specific grouping. + +### Data Model + +A `Timeline` is a lightweight container: + +- **name** — short label shown in the pill (e.g., "Agents", "Phases", "Tools") +- **description** — tooltip or subtitle explaining the view +- **root** — a `TimelineSpan` tree (init events folded into root, scoring as a child span) + +### UI: Timeline Pills + +When multiple timelines are available, a row of pills appears above the timeline panel. Clicking a pill switches the entire timeline and content panel to that view. Only one timeline is active at a time. + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ [ Agents ] [ Phases ] [ Tools ] │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Transcript 48.5k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Transcript│██████████████████████████████████████████████████████│ │ +│ Explore │ ███████████ │ 8.1k │ +│ Plan │ ████████ │ 5.3k │ +│ Build │ ████████████████████████████ │ 31.8k │ +│ Scoring │ ███│ 3.2k │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### Behavior + +- **Single timeline**: pills are hidden — no UI change from current design +- **Multiple timelines**: pills appear; the first timeline is selected by default +- Switching timelines resets drill-down, selection, and branch navigation to the new tree's root +- Each timeline maintains independent navigation state while active +- The active timeline pill is visually highlighted (filled background vs outline) + +## 13. Custom Outline + +By default, the content panel shows events in a flat chronological list. When a `TimelineSpan` has an `outline` attached, a sidebar appears on the left side of the content panel providing hierarchical navigation. This is primarily useful for custom timelines where the author wants to impose a meaningful structure on the event stream. + +### Data Model + +An `Outline` is a tree of `OutlineNode` entries, each referencing an event by UUID: + +- **event** — UUID of an event in the agent's content +- **children** — optional nested `OutlineNode` list + +### Layout + +The outline sidebar sits to the left of the content panel, separated by a vertical divider. Outline entries are shown as a collapsible tree. Clicking an entry scrolls the content panel to that event. + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ ← Transcript › Build 31.8k tokens │ +├──────────────────────────────────────────────────────────────────────────┤ +│ Build │██████████████████████████████████████████████████████│ │ +│ Code │ ██████████████████████ │ 15.2k │ +│ Test │ ████████████████ │ 10.4k │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ │ +│ Outline │ ◆ MODEL 2.1k │ +│ │ I'll start by reading the existing codebase... │ +│ ▾ Setup │ │ +│ Read │ ◇ TOOL read_file 0.3s │ +│ Config │ path: "src/main.py" │ +│ ▾ Impl │ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │ +│ Code │ import os │ +│ Test │ from app import create_app │ +│ ▸ Review │ ... │ +│ │ │ +│ │ ◆ MODEL 3.4k │ +│ │ Based on the code, I need to modify the auth... │ +│ │ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### Behavior + +- **No outline**: sidebar is hidden — content panel uses full width (default) +- **With outline**: sidebar appears; entries are collapsible tree nodes +- Clicking an outline entry scrolls the content panel to the referenced event and highlights it +- The currently visible event is highlighted in the outline (scroll tracking) +- Outline entries display the event's label (model output preview, tool name, etc.) + +# Timeline Implementation Briefing + + +Reference material for implementing the timeline UI. Captures the design spec, data model, and application architecture context. + +## 1. Design Spec Summary (timeline-ui.md) + +Please read the timeline-ui.md as required for more details. + +The timeline UI visualizes transcript execution as a swimlane-based timeline with a content panel below. Key concepts: + +### Timeline Panel + +- **Swimlane rows** for agents, each showing a proportional bar with token count. A full-width Transcript bar spans the top. +- **Iterative agents** appear as multiple non-contiguous spans on one row, with aggregated token counts. +- **Parallel agents** of the same type collapse into a single row with a count badge (e.g., `Explore (3)`). +- **Drill-down navigation** instead of showing all nesting levels. Clicking a sub-agent rescales the timeline to fill full width, with breadcrumb navigation (`← Transcript > Build > Test`). Escape goes up. +- **Inline markers**: `▲` for errors, `┊` for compaction, `↳` for branch fork points — placed inline within bars. +- **Selection vs. drill-down**: Single-click selects a row (drives the content panel). `›` chevron or double-click drills down. Arrow keys move between rows. +- **Adaptive height**: Grows to fit rows up to a cap of 6, then scrolls internally. Resizable divider between timeline and content panel. + +### Content Panel + +- Shows the selected agent's events chronologically: `◆ MODEL` calls, `◇ TOOL` calls, and sub-agent launch cards. +- **Sub-agent cards** appear inline at launch points, showing name, tokens, duration, and task description preview. Clickable to drill down. +- **Parallel sub-agents** grouped with a `┃` gutter connecting their cards. +- **Errors**: `▲ ERROR` badge on tool events. **Compaction**: horizontal divider. +- **Utility agents**: Hidden by default behind a toggle (`▸ 12 utility agents hidden`), shown as single-line `⚙` entries when revealed. +- **Branch cards**: Dashed borders to distinguish from child agent cards, appearing at fork points. + +### Multiple Timelines + +- Pill selector above the timeline for switching between named views (e.g., "Agents", "Phases", "Tools"). +- Each timeline has its own `Timeline` with a root `TimelineSpan` tree. +- Switching resets drill-down, selection, and branch navigation. + +### Custom Outline + +- Optional sidebar in the content panel when a `TimelineSpan` has an `outline`. +- Collapsible tree with scroll-tracking and click-to-scroll. + +## 2. Data Model (timeline.ts) + +The TypeScript data model that powers the timeline: + +### Core Types + +```typescript +interface TimelineEvent { + type: "event"; + event: Event; + startTime: Date; + endTime: Date; + totalTokens: number; +} + +interface TimelineSpan { + type: "span"; + id: string; + name: string; + spanType: string | null; // "agent", "scorer", "tool", or null + content: (TimelineEvent | TimelineSpan)[]; + branches: TimelineBranch[]; + utility: boolean; + outline?: Outline; + startTime: Date; + endTime: Date; + totalTokens: number; +} + +interface TimelineBranch { + type: "branch"; + forkedAt: string; // UUID of the fork-point event + content: (TimelineEvent | TimelineSpan)[]; + startTime: Date; + endTime: Date; + totalTokens: number; +} + +interface Timeline { + name: string; + description: string; + root: TimelineSpan; +} + +interface Outline { + nodes: OutlineNode[]; +} + +interface OutlineNode { + event: string; // UUID reference + children?: OutlineNode[]; +} +``` + +### Processing Pipeline + +`buildTimeline(events: Event[]) -> Timeline`: + +1. Build span tree from flat events (span_begin/span_end pairing) +2. Find phase spans (init/solvers/scorers) or treat entire stream as agent +3. Init events are folded into the root `TimelineSpan.content` as early events +4. Build agent hierarchy from explicit `type='agent'` spans or tool-spawned agents +5. Scoring spans become `TimelineSpan` children with `spanType: "scorer"` +6. Classify utility agents (single-turn + different system prompt from parent) +7. Detect branches (explicit `type='branch'` spans or auto-detected via duplicate input fingerprints) + +## 3. Application Architecture + +### Stack + +- **React 19** + React Router 7 (hash-based routing) +- **VSCode Elements** for native VSCode-like controls +- **Bootstrap 5** (themed for VSCode appearance) for layout/utilities +- **Zustand 5** with immer + persist for state management +- **React Query v5** for server state with `AsyncData` wrapper +- **Virtuoso** / TanStack Virtual for virtualized lists +- **CSS Modules** for component-scoped styling + +### Directory Structure + +``` +src/ +├── api/ # ScoutApiV2 interface + HTTP/JSON-RPC implementations +├── app/ +│ ├── transcript/ # TranscriptPanel, TranscriptBody (tabs: Messages, Events, Metadata, Info) +│ ├── timeline/ # TimelinePanel (placeholder) + syntheticNodes.ts (10 test scenarios) +│ └── server/ # React Query hooks (useTranscript, useScans, etc.) +├── components/ +│ ├── transcript/ # Rendering engine: timeline.ts, outline, event viewers, transforms +│ └── chat/ # Chat message rendering +├── state/ # Zustand store (single store, multiple slices) +├── router/ # URL helpers, route definitions +└── types/ # TypeScript types (including generated.ts from OpenAPI) +``` + +### State Management + +Single Zustand store with slices for: app status, scans, transcripts, UI state (scroll positions, collapsed events, grid states), validation. Uses immer for immutable updates and persist for localStorage. + +### Data Flow + +- `ApiProvider` exposes `ScoutApiV2` interface (server HTTP or VSCode JSON-RPC) +- React Query hooks wrap API calls in `AsyncData` (loading | data | error) +- WebSocket topic-based invalidation for real-time updates +- `staleTime: Infinity` for static data like transcripts + +### Existing Transcript Rendering + +1. Raw events fetched via API +2. `buildTimeline()` builds the semantic tree as a `Timeline` with root `TimelineSpan` +3. `transform()` / `flatten()` / `treeify()` prepare the display tree +4. `TranscriptVirtualList` renders via Virtuoso with event-specific viewers +5. `TranscriptOutline` provides tree sidebar navigation + +### Timeline Current State + +- `TimelinePanel` at `/timeline` route — placeholder with scenario dropdown +- `syntheticNodes.ts` provides 10 mock `Timeline` scenarios (sequential, iterative, parallel, branches, utilities, deep nesting, etc.) +- No visual implementation yet + +### Key Patterns + +- Module CSS for component scoping +- `AsyncData` discriminated union for loading/data/error +- VSCode Elements for form controls and split layouts +- Bootstrap for grid/layout utilities +- Virtualized rendering for large datasets + +# Timeline Implementation Design + +Design decisions and architecture for implementing the timeline UI. + +## 1. State Management + +### URL State (via `replace` navigation) + +Structural navigation state lives in URL query params, updated via `navigate(..., { replace: true })` so no browser history entries are created. The timeline's own navigation (breadcrumbs, Escape, chevrons) controls drill-down; browser back controls page-level navigation. + +Parameters (all lowercase, snake_case for multi-word): + +- `path=build/test` — drill-down breadcrumb path (agent names joined by `/`) +- `selected=code` — which row is selected +- `timeline_view=agents` — which timeline pill is active (only when multiple timelines exist) + +Agent names in URL params are **case-insensitive** — resolved against `TimelineSpan.name` via `toLowerCase()` comparison. + +### Zustand State (persisted) + +Internal UI state lives in Zustand, persisted for VSCode tab restore: + +- Scroll position in the content panel +- Resizable divider position (timeline vs. content panel height) +- Utility agents toggle (shown/hidden) +- Collapsed state within the content panel +- Visible range tracking for virtualized lists + +Zustand keys are scoped by transcript ID + drill-down path (e.g., `timeline:${transcriptId}:build/test`) so each drill-down level maintains independent UI state. Multiple scopes coexist simultaneously — drilling into `build`, scrolling, then going back to root preserves the scroll position at both levels. + +**Cleanup:** All scoped entries for a transcript ID are cleared when the user navigates away from that transcript. Within a session, entries accumulate freely (bounded by the number of drill-down levels visited, which is small). + +### Restoration Priority + +On mount (including VSCode tab restore): + +1. URL params are the source of truth for structural navigation state +2. Zustand provides fine-grained UI state (scroll, collapse, divider position) + +## 2. Data Architecture + +### `useTimeline` Hook + +A custom hook encapsulates tree building, path resolution, and navigation actions. The container calls the hook and distributes results to the Timeline and ContentPanel components. + +``` +Container + ├── const timeline = useTimeline(events) + ├── passes tree + navigation actions to Timeline + ├── passes resolved node to ContentPanel +``` + +**Input:** `Timeline` (pre-built). Tree building happens upstream — typically a `useMemo` in the container: + +```typescript +const tl = useMemo(() => buildTimeline(events), [events]); +const timeline = useTimeline(tl); +``` + +For multiple timelines, the container selects which one to pass: + +```typescript +const timelines = useMemo(() => buildTimelines(events), [events]); +const timeline = useTimeline(timelines[activeIndex]); +``` + +**Responsibilities:** +- Reads URL params (`path`, `selected`) to determine navigation state +- Resolves the path to a specific `TimelineSpan` (case-insensitive matching) +- Computes swimlane rows from the resolved node (separate `useMemo`) +- Provides navigation actions that update URL via `replace` + +**Return value (sketch):** +```typescript +const timeline = useTimeline(tl); +timeline.node // resolved TimelineSpan for current path +timeline.rows // SwimLaneRow[] for the current drill-down level +timeline.breadcrumbs // path segments for breadcrumb UI +timeline.selected // which row is selected +timeline.drillDown(agentName) // navigate into a child agent/span +timeline.goUp() // navigate to parent +timeline.select(agentName) // select a row +``` + +**Dependencies:** The hook reads/writes URL params internally (via `useSearchParams` / `useNavigate`). It does not take `transcriptId`, `Timeline[]`, or a navigate function. + +**Benefits:** +- Single source of truth for path resolution + swimlane computation +- Plays naturally with URL-based state (hook reads URL params, returns resolved state) +- Both Timeline and ContentPanel become relatively simple renderers +- Independently testable without DOM (hook tests) +- Follows existing patterns (`useTranscript`, `useAdjacentTranscriptIds`) + +## 3. Content Panel + +### Strategy: Enhance Existing Components + +The content panel reuses and enhances the existing `TranscriptView` and `TranscriptOutline` components rather than building new ones. + +### Component Composition (Prototype) + +The initial prototype composes three components: + +``` +TimelineContainer + ├── const timeline = useTimeline(events) + ├── TranscriptTimeline ← new (swimlane bars, breadcrumbs) + ├── resizable divider + └── ContentPanel + ├── TranscriptOutline ← enhanced existing + └── TranscriptView ← enhanced existing +``` + +These are composed as a standalone prototype first, then integrated back into the mainline `TranscriptBody` once stable. + +### Enhancements Required + +**TranscriptView:** +- Accept a `TimelineSpan` (not just raw `Event[]`) as input +- Render child `TimelineSpan`s inline as sub-agent cards (clickable) +- Render `TimelineBranch` cards with dashed borders at fork points +- Sub-agent card clicks call `timeline.drillDown()` — navigation flows through the `useTimeline` hook +- Error and compaction markers are not needed in the content panel — these are already visible as regular events in the content stream + +**TranscriptOutline:** +- Accept a `TimelineSpan` to build its tree (currently works from raw events) +- Support custom `Outline` attached to a `TimelineSpan` (replaces the auto-generated outline) +- When a custom outline exists, use its `OutlineNode` tree directly instead of the visitor-based pipeline + +### Utility Agent Visibility + +The utility agent toggle (`▸ N utility agents hidden` / `▾ N utility agents shown`) lives in the surrounding chrome, not inside the content panel. Utility agent visibility is a Zustand store concern — the content panel simply renders whatever content it receives. Filtering happens upstream before events reach the content panel. + +### Navigation Flow + +Clicking a sub-agent card in the content panel is a drill-down navigation: + +``` +User clicks "Build" card in ContentPanel + → calls timeline.drillDown("build") + → URL updated via replace: ?path=build + → useTimeline resolves new path → new TimelineSpan + → Timeline re-renders with new breadcrumbs + child rows + → ContentPanel re-renders with Build's content +``` + +This unifies navigation — whether the user clicks a swimlane row chevron in the timeline or a sub-agent card in the content panel, the same `useTimeline` hook handles it. + +## 4. Swimlane Row Computation + +### Data Model + +A swimlane row is a sequence of spans sharing an agent name. Each span is either a single agent or a parallel cluster of agents: + +```typescript +interface SingleSpan { + agent: TimelineSpan; +} + +interface ParallelSpan { + agents: TimelineSpan[]; +} + +type RowSpan = SingleSpan | ParallelSpan; + +interface SwimLaneRow { + name: string; + spans: RowSpan[]; + totalTokens: number; + startTime: Date; + endTime: Date; +} +``` + +Properties like `kind` (single/iterative/parallel) and `drillable` (has children) are not stored — they are derivable from the data and computed as needed by consumers. + +### Grouping Algorithm + +1. Collect child `TimelineSpan`s from the current node's `content` (skip `TimelineEvent`s and utility spans) +2. Group by name (case-insensitive) +3. Within each name group, cluster into spans: + - Non-overlapping spans become separate `SingleSpan` entries on the row (iterative pattern) + - Overlapping spans (within a tolerance of ~100ms to account for spawn skew) become a `ParallelSpan` + - If any overlap exists within the name group, the entire group is treated as parallel + - A row can mix single and parallel spans (e.g., a single Explore, then later three Explore in parallel) +4. Order rows by earliest start time across their spans +5. The parent `TimelineSpan` is always the top row (full-width bar) +6. Scoring is a standard child `TimelineSpan` with `spanType: "scorer"` — appears as a regular swimlane row + +### Parallel Count Display + +The parallel count badge appears on the bar (span), not the row label. This handles cases where the same agent name has different levels of parallelism across invocations — e.g., the label just says "Explore" while one span shows `(3)` and another shows `(2)`. + +### Drill-Down + +- Each span has its own chevron for drill-down +- No row-level drill-down — only individual spans are drillable +- For parallel spans, drilling in reveals the individual agent instances (per design doc section 4) +- For single spans, drilling in reveals that agent's children + +## 5. Timeline Swimlane Rendering + +### Approach: Hybrid DOM + CSS Background + +The timeline uses a DOM-based layout with visual fills as separate positioned elements. All elements within a row share one coordinate space (the bar area width), making cross-span positioning (e.g., branch fork markers) straightforward. + +### Layout Structure + +Each swimlane row is a three-column CSS Grid: + +``` +┌─────────┬──────────────────────────────────────────┬───────┐ +│ label │ bar area │ tokens│ +└─────────┴──────────────────────────────────────────┴───────┘ +``` + +Within the bar area, all elements are absolutely positioned using percentage-based `left`/`width` relative to the bar area container: + +```html +
+ +
+
+ + + + + + + + + +
+``` + +### Why Hybrid Over Pure DOM + +In pure DOM, each bar segment is both the visual fill and the interactive container, with markers positioned relative to their segment. This creates problems for elements that aren't scoped to a single segment — e.g., a `↳` branch marker on the parent bar at a time position between child spans. The hybrid approach puts everything in one coordinate space, so positioning any element is just `left: ` regardless of which fill it overlaps. + +Click targets for bar segments use either click handlers on the fill divs or invisible overlay divs — the visual and interactive layers are separate. + +### Proportional Positioning + +All `left`/`width` percentages are computed from timestamps relative to the current view's time range: + +``` +percent = (timestamp - viewStart) / (viewEnd - viewStart) * 100 +``` + +Where `viewStart`/`viewEnd` come from the current drill-down level's `TimelineSpan` time range. + +### Theming + +Fills, markers, and selection highlighting use CSS custom properties (VSCode theme variables) for light/dark mode compatibility. + +## 6. `useTimeline` Hook — Detailed Behavior + +### Edge Cases + +- **Invalid path**: If the URL `path` param doesn't resolve to a valid node (e.g., stale URL from a different transcript), fall back to the root. No error state — just silently reset. +- **Flat transcript** (no agents): The single root `TimelineSpan` is the parent row and the content source. Timeline shows one bar. + +### Selection Semantics + +Selecting a row **fully replaces** the content panel with that node's content. It does not scroll within the parent's events — the selected node's `TimelineSpan` becomes the content panel's data source. + +### Init and Scoring + +Init events are folded into the root `TimelineSpan.content` as early events — there is no separate init section. Scoring is a child `TimelineSpan` with `spanType: "scorer"` that appears naturally in the root's content and as a swimlane row. + +### Span Numbering for Drill-Down + +When an agent name has multiple spans (iterative or parallel), spans are numbered with a `-N` suffix for disambiguation in URL paths: + +- `?path=build` — single Build agent, no suffix needed +- `?path=explore-2` — second span of Explore +- `?path=explore-2/test` — drill into Test within the second Explore invocation + +Resolution rules: +- Find all agents/spans matching the name (case-insensitive) +- If a `-N` suffix is present, select the Nth span (1-indexed) +- If no suffix and only one match, use it +- If no suffix and multiple matches, default to the first + +This numbering applies uniformly to both iterative spans (sequential invocations) and parallel spans (individual instances within a parallel group). + +### Memoization Strategy + +- `buildTimeline(events)` — memoized via `useMemo` keyed on `events` +- Swimlane row computation — separate `useMemo` keyed on the resolved `TimelineSpan`, deferred from the tree build +- Navigation actions (`drillDown`, `goUp`, `select`) — stable callbacks via `useCallback` + +## 7. Header Bar + +### Structure + +A `TimelineHeader` component composed of two sub-components: + +``` +TimelineHeader + ├── TimelinePills ← conditional (only when multiple timelines) + └── TimelineBreadcrumb ← always present +``` + +### TimelinePills + +A row of pill buttons for switching between named timeline views. Only rendered when multiple timelines exist — single-timeline transcripts show no pills. + +### TimelineBreadcrumb + +A single row with three elements: + +``` +← Transcript › Build › Test 31.8k tokens +``` + +- **Back arrow** (`←`): Visible when drilled in (not at root). Goes up one level (same as Escape). +- **Breadcrumb segments**: Each segment is clickable, jumping directly to that level. Allows skipping levels (e.g., from `Build > Test` directly to root by clicking `Transcript`). +- **Token count**: Right-aligned. Shows the current drill-down node's total tokens (not the full transcript total). + +At root level, just the name and total: + +``` +Transcript 48.5k tokens +``` + +### Data Flow + +- `TimelinePills` receives the `Timeline[]` array and active index from the container +- `TimelineBreadcrumb` receives `timeline.breadcrumbs`, `timeline.node.totalTokens`, and `timeline.goUp` from the `useTimeline` hook +- Breadcrumb segment clicks call `timeline.drillDown` or navigate to a specific path level + +## 8. Keyboard Navigation + +### Focus Model + +The timeline panel is a focusable container (`tabIndex={0}`). Keyboard events are only handled when the timeline has focus — no global key handlers. Clicking a row or tabbing into the timeline gives it focus. + +### Key Bindings + +| Key | Effect | +|-----|--------| +| `↑` / `↓` | Move selection between rows (including the parent row at top) | +| `Enter` | Drill down into selected row | +| `Escape` | Go up one level; if at root with selection, deselect; if at root with no selection, no-op | + +### Behavior Details + +- **Arrow keys stop at boundaries** — pressing `↓` on the last row or `↑` on the first row does nothing (no wrapping). +- **Parent row is included** in the arrow key cycle — `↑` from the first child selects the parent. +- **No-ops are silent** — Enter on a non-drillable row (e.g., Scoring with no children) does nothing. No visual feedback. +- **Escape does not cross boundaries** — Escape in the content panel does not move focus back to the timeline. The two panels have independent focus scopes. + +## 9. Content Panel ↔ TimelineSpan Integration + +### Approach: Direct rendering from TimelineSpan content + +The content panel builds a flat rendering list directly from `TimelineSpan.content`, bypassing the existing transform pipeline (no fixups, treeify, transform, flatten). The `TimelineSpan` is already the structured form — the transform pipeline exists to build structure from raw events, which is unnecessary here. + +However, the existing pipeline encodes implicit rules and behaviors (e.g., collapsing pending events, sandbox event grouping, `sample_init` injection, default collapse decisions, span unwrapping). These rules must be audited during implementation — some are irrelevant in the TimelineSpan context (the tree is already structured), but others may need to be reflected in the new rendering path. Document each rule as it's encountered. + +### Content Item Types + +A discriminated union for the virtual list: + +```typescript +type ContentItem = + | { type: "event"; eventNode: TimelineEvent } + | { type: "agent_card"; agentNode: TimelineSpan } + | { type: "branch_card"; branch: TimelineBranch } + | { type: "parallel_group"; agents: TimelineSpan[] } +``` + +### Building the Content List + +Walk `TimelineSpan.content` chronologically: + +1. **TimelineEvent** items → `{ type: "event", eventNode }` +2. **Child TimelineSpan** items → `{ type: "agent_card", agentNode }` (or `parallel_group` if consecutive same-name spans overlap) +3. **Branches** → insert `{ type: "branch_card", branch }` at each branch's `forkedAt` position + +### Rendering Dispatch + +The virtual list dispatches on `ContentItem.type`: + +- `"event"` → delegates to existing event viewers (`ModelEventView`, `ToolEventView`, etc.) via `RenderedEventNode`, passing `eventNode.event`. No changes to existing viewers needed. +- `"agent_card"` → new component: sub-agent card with name, tokens, duration, task description preview. Solid border. Clickable — calls `timeline.drillDown()`. +- `"branch_card"` → new component: branch card with `↳` prefix, dashed border. Clickable — drills into the branch. +- `"parallel_group"` → new component: stacked agent cards connected with `┃` gutter. Each card individually clickable. + +## 10. Inline Marker Computation + +### Marker Types + +- **`▲` Error** — `ToolEvent` with error result, `ModelEvent` with error output. Classified by a function `isErrorEvent(event: Event) => boolean`. +- **`┊` Compaction** — `CompactionEvent` (explicit event type in the inspect event stream). +- **`↳` Branch** — from `TimelineSpan.branches`. Each branch's `forkedAt` UUID is resolved to a timestamp. + +### Scanning Depth + +The depth of marker collection is a user-configurable option: + +| Setting | Behavior | +|---------|----------| +| `"direct"` | Only events in the node's own `content` | +| `"children"` | Direct content + one level of child agents (default) | +| `"recursive"` | Full subtree scan | + +Default is `"children"` — shows that an error happened in a direct child without flooding markers from deeply nested sub-agents. + +The depth preference is stored in Zustand (persisted display preference), not in the URL. + +### Computation + +Markers are computed on render via `useMemo`, keyed on the agent node and depth setting: + +```typescript +const markers = useMemo( + () => collectMarkers(node, depth), + [node, depth] +); +``` + +No pre-computation on swimlane rows — avoids computing multiple depth variants and keeps the row data model simple. The scan is cheap (small number of events per view level). + +### Positioning + +Each marker has a timestamp, mapped to a percentage position using the same formula as bar fills: + +``` +percent = (marker.timestamp - viewStart) / (viewEnd - viewStart) * 100 +``` + +### Branch Markers + +Branch markers (`↳`) are separate from error/compaction markers. They come from `TimelineSpan.branches` — each branch's `forkedAt` UUID is resolved to the corresponding event's timestamp. Branch markers only appear on the row that owns the branches, not aggregated upward. + +## 11. Resizable Divider + +### Layout + +The timeline panel and content panel are separated by a VSCode Elements split layout divider (`VscodeSplitLayout`). The timeline panel sits above, the content panel below. + +### Default Height + +The timeline panel shows up to **5 rows** by default (parent row + up to 4 child rows). If there are more rows, the timeline area scrolls internally. The divider allows the user to customize the split. + +### Persistence + +The divider position is stored in Zustand (persisted) so it survives VSCode tab switches and session restarts. + + +# Phased Implementation + +All new files go in `src/inspect_scout/_view/www/src/app/timeline/`. + +## Working Guidelines + +1. **One phase at a time.** Implement, test, and verify each phase before moving to the next. +2. **Review before commit.** After tests pass, pause and review the code together before committing. Do not auto-commit. +3. **Full tests at each step.** Every phase produces both an implementation file and a test file. Run `pnpm test` and `pnpm check` to verify. +4. **Use synthetic scenarios.** Import `timelineScenarios` from `syntheticNodes.ts` for realistic test data. Build minimal inline helpers for edge cases. +5. **Pure logic first.** Phases 1–3 are pure functions with no DOM or React dependencies. Phase 4 introduces the hook layer. +6. **Mirror Python changes.** If a phase touches `timeline.ts`, the corresponding changes must be mirrored in `timeline.py` and both test suites must pass. + +## Phase 1: Swimlane Row Computation (Complete) + +**Files:** `swimlaneRows.ts`, `swimlaneRows.test.ts` +**Commit:** `b8b384ed` + +### What was done + +Implemented `computeSwimLaneRows(node: TimelineSpan): SwimLaneRow[]` which transforms a TimelineSpan's children into rows for rendering as horizontal swimlane bars. + +**Types:** +- `SingleSpan { agent: TimelineSpan }` — one span occupying a time range +- `ParallelSpan { agents: TimelineSpan[] }` — overlapping spans on the same row +- `RowSpan = SingleSpan | ParallelSpan` — with `isSingleSpan()` / `isParallelSpan()` guards +- `SwimLaneRow { name, spans, totalTokens, startTime, endTime }` + +**Algorithm:** +1. Parent row first (the node itself as a SingleSpan) +2. Filter children to non-utility TimelineSpans +3. Group by name (case-insensitive, display name from first encountered) +4. Cluster spans: check pairwise overlap with 100ms tolerance — any overlap makes the entire group a ParallelSpan, otherwise each span is a separate SingleSpan +5. Order rows by earliest start time +6. Scoring is a regular child TimelineSpan with `spanType: "scorer"` — no special handling needed + +### Non-nullable times + +`startTime`/`endTime` are non-nullable across both Python (`timeline.py`) and TypeScript (`timeline.ts`), since every Event has a required timestamp field. Container nodes use an epoch sentinel (`new Date(0)` / `datetime(1970, 1, 1, tzinfo=timezone.utc)`) for the degenerate empty-content case. + +**Tests:** 20 tests covering sequential (S1), iterative (S2), parallel (S4), flat (S7), many-rows (S8), utility filtering (S10), custom edge cases (case-insensitive grouping, no children, token aggregation, time ranges). + +## Phase 2: Content Item Building (Complete) + +**Files:** `contentItems.ts`, `contentItems.test.ts` +**Commit:** `3401bfca` + +### What was done + +Implemented `buildContentItems(node: TimelineSpan): ContentItem[]` which transforms a TimelineSpan into a flat list of items for the detail panel. + +**Types:** +- `EventItem { type: "event", eventNode }` — a single event +- `AgentCardItem { type: "agent_card", agentNode }` — a child agent rendered as a card +- `BranchCardItem { type: "branch_card", branch }` — a branch fork point +- `ContentItem` — discriminated union of the above + +**Algorithm:** +1. Walk `node.content` chronologically: TimelineEvent → EventItem, TimelineSpan → AgentCardItem +2. Insert branch cards: for each branch, find the event matching `forkedAt` UUID and insert a BranchCardItem after it. Multiple branches at the same fork point appear consecutively. Unresolvable UUIDs → append at end. + +No parallel grouping type — parallel agents appear as consecutive AgentCardItems. The UI layer detects adjacency and renders visual grouping. Utility agents are always included; filtering is a UI concern. + +**Tests:** 16 tests covering sequential (S1), flat (S7), parallel (S4), iterative (S2), utility agents (S10, drilled into Build), deep nesting (S3), branches with unmatched UUIDs (S11a, S11b), matched UUID insertion, multiple branches at same fork point, branches at different positions, mixed matched/unmatched, edge cases. + +## Phase 3: Marker Computation (Complete) + +**Files:** `markers.ts`, `markers.test.ts` +**Commit:** `4c4b84d5` + +### What was done + +Implemented `collectMarkers(node: TimelineSpan, depth: MarkerDepth): TimelineMarker[]` which finds error, compaction, and branch markers at configurable depth. + +**Types:** +- `MarkerKind = "error" | "compaction" | "branch"` +- `TimelineMarker { kind, timestamp, reference }` — reference is event UUID or forkedAt ID +- `MarkerDepth = "direct" | "children" | "recursive"` + +**Exported helpers:** +- `isErrorEvent(event)` — ToolEvent with `.error !== null`, or ModelEvent with `.error !== null` or `.output.error !== null` +- `isCompactionEvent(event)` — `event.event === "compaction"` + +**Algorithm:** +- Recursion controlled by `shouldDescend(depth, currentLevel)`: direct = never descend, children = descend at level 0 only, recursive = always descend +- Branch markers resolved by scanning `node.content` for the event matching `forkedAt` UUID → timestamp. Unresolvable branches silently dropped. +- All markers sorted by timestamp. + +**Tests:** 23 tests covering isErrorEvent (6 cases: ToolEvent with/without error, ModelEvent with event.error, output.error, clean, CompactionEvent), isCompactionEvent (3 cases), S5 inline markers (error + compaction from child agent), S7 flat (empty), depth modes (direct/children/recursive with parent-child-grandchild), branch markers (matched UUID, unmatched, empty forkedAt, S11a synthetic), sort order (unsorted input, mixed types), edge cases (empty agent, normal-only events). + +## Phase 4: `useTimeline` Hook (Complete) + +**Files:** `useTimeline.ts`, `useTimeline.test.ts` + +### What was done + +Implemented the `useTimeline` hook and three pure helper functions that drive URL-based drill-down navigation through the transcript node tree. + +**Pure functions** (exported, testable without DOM): + +- `parsePathSegment(segment)` — splits `"my-agent-3"` into `{ name: "my-agent", spanIndex: 3 }`. Only the trailing `-N` is consumed (N must be >= 1). `-0` is treated as part of the name. +- `resolvePath(timeline, pathString)` — walks the tree from `timeline.root`, splitting path on `/`, matching child spans case-insensitively. `-N` suffix selects the Nth same-named child (1-indexed). Returns `null` for invalid paths. +- `buildBreadcrumbs(pathString, timeline)` — builds `BreadcrumbSegment[]` starting with root. Each segment resolves its label from the actual span name when possible, falls back to the raw segment string. + +**Hook:** `useTimeline(timeline: Timeline): TimelineState` + +- Reads `path` and `selected` from `useSearchParams()` +- Resolves path → `TimelineSpan` +- Falls back to root on invalid path +- Init events are already in root content; scoring is a child `TimelineSpan` with `spanType: "scorer"` — no special folding needed +- Computes `rows` via `computeSwimLaneRows()`, `breadcrumbs` via `buildBreadcrumbs()` +- Navigation: `drillDown(name, spanIndex?)`, `goUp()`, `select(name | null)` — all update URL via `setSearchParams(..., { replace: true })` + +**Tests:** Pure function tests (no jsdom): parsePathSegment (7 cases), resolvePath (12 cases including root, named, case-insensitive, nested S3, span index S2, invalid, scoring, init, out-of-range), buildBreadcrumbs (5 cases). Hook tests (jsdom + `MemoryRouter` wrapper): S1 root resolution, drill-down, S7 flat, S4 parallel, selected param, nested breadcrumbs, invalid fallback, scoring as child span, drillDown/goUp/select navigation, span index drill-down, S2 iterative spans. + +## Phase 5: Timeline UI Components (Complete) + +### Phase 5a: Swimlane Layout + +**Files:** `swimlaneLayout.ts`, `swimlaneLayout.test.ts` + +Implemented `computeRowLayouts(rows, viewStart, viewEnd)` which positions swimlane bars as percentage-based rectangles within a time range. + +**Types:** +- `PositionedBar { left, width, agent, drillable }` — a bar with percentage position, the source `TimelineSpan`, and whether it has children to drill into +- `PositionedMarker { left, kind, reference }` — a marker glyph at a percentage position. `reference` carries the event UUID (for error/compaction) or `forkedAt` UUID (for branch markers), enabling click handlers to look up related data. +- `RowLayout { name, bars, markers, tokenLabel }` — complete layout for one swimlane row + +**Algorithm:** +- `computeBarPosition(start, end, viewStart, viewEnd)` — clamps to `[0, 100]` percentage range +- Drillability: a span is drillable if it has child spans (`content.some(c => c.type === "span")`) +- Token labels use `formatTokenCount()` (e.g., `"8.1k"`, `"1.2M"`) +- Markers collected via `collectMarkers(parentSpan, "children")` and positioned using the same percentage formula + +**Tests:** 15 tests covering S1 sequential (positions, drillability, token labels, markers), S2 iterative (multiple bars per row), S4 parallel, S7 flat, marker positioning, edge cases. + +### Phase 5b: Timeline Header Components + +**Files:** `TimelineBreadcrumb.tsx`, `TimelineBreadcrumb.module.css`, `TimelinePills.tsx`, `TimelinePills.module.css`, `TimelineMinimap.tsx`, `TimelineMinimap.module.css` + +**TimelineBreadcrumb:** Renders `← Transcript › Build › Refactor` with clickable segments. Uses `BreadcrumbSegment[]` from `buildBreadcrumbs()`. The `←` back button navigates up one level. Duration displayed on the right via `formatDuration()` from `utils/format.ts`. + +**TimelinePills:** Shows aggregate stats (total tokens, duration, model name) as small pill badges in the header area. + +**TimelineMinimap:** A thin horizontal strip showing the current zoom position within the root timeline when drilled into a child span. Rendered inside the swimlane grid using `display: contents` to participate in the 3-column layout. Shows a 2px rail track with two vertical blue bar markers at the zoom boundaries (the drilled-into span's start/end relative to the root). Uses `computeBarPosition()` for percentage positioning. Hidden at root level (when the view spans the full timeline). + +### Phase 5c: SwimLane Panel + +**Files:** `SwimLanePanel.tsx`, `SwimLanePanel.module.css` + +The main swimlane visualization component rendering a CSS grid with three columns: labels (left), bars (center), token counts (right). + +**Features:** +- **Row rendering:** Each `RowLayout` renders as a grid row. Parent row has a distinct background. Child rows show bar fills with percentage-based `left`/`width` styling. +- **Selection:** Clicking a row sets `selected` in URL search params. Selected row gets a highlight ring. Clicking the selected row deselects it. +- **Keyboard navigation:** Arrow keys (`↑`/`↓`) cycle through rows, `Enter` drills into a drillable row, `Escape` clears selection. Parent row is included in the arrow key cycle. +- **Marker glyphs:** Inline markers (`▲` error, `┊` compaction, `↳` branch) rendered at their percentage positions on the bar area. +- **Branch popover:** Clicking a `↳` branch marker opens a popover listing branches at that fork point. Uses the existing `PopOver` component with `hoverDelay={-1}` (click-to-open pattern). Each entry shows branch label, token count, and duration. Clicking an entry drills into the branch. +- **Branch lookup:** `findBranchesByForkedAt(content, forkedAt)` recursively searches the span tree for branches matching a `forkedAt` UUID, returning both the branches and the owner span's path. This is necessary because branches may be on deeply nested child spans, not the currently viewed node. +- **Max height:** CSS variable `--swimlane-minimap-height: 14px` accounts for the minimap in the max-height calculation, preventing scrolling when 5 rows plus minimap are displayed. + +### Phase 5d: Timeline Panel + +**Files:** `TimelinePanel.tsx` + +Top-level panel wiring `useTimeline` state to the UI components. Passes `node`, `rows`, `breadcrumbs`, and navigation callbacks (`drillDown`, `goUp`, `select`) to child components. Connects `onBranchDrillDown` from SwimLanePanel to `state.drillDown`. Passes root timeline start/end times to the minimap for zoom position calculation. + +### Phase 5e: Branch Navigation + +**Files:** `useTimeline.ts` (extended), `useTimeline.test.ts` (extended) + +Extended the path system to support drilling into branches via `@branch-N` path segments, where N is the 1-indexed branch index among branches sharing the same `forkedAt` UUID. + +**Path resolution:** `resolveBranchSegment(segment, parent)` detects `@branch-` prefix, parses the index, looks up `parent.branches`, and returns a synthetic span wrapping the branch content via `createBranchSpan`. + +**`createBranchSpan` optimization:** For single-span branches (branch content contains exactly one child span), returns the child span directly with a `↳` prefix on its name. This avoids double-rendering where both a synthetic wrapper and the single child would appear as separate swimlane rows. + +**`deriveBranchLabel`:** Uses the first child span's name if one exists (e.g., "Refactor"), otherwise falls back to "Branch N". + +**Breadcrumb support:** `buildBreadcrumbs` handles `@branch-N` segments by calling `resolveBranchSegment` to get the resolved label (e.g., `↳ Refactor`) instead of showing the raw path segment. + +**Tests:** 5 new tests: branch path resolution (valid index, out-of-range), breadcrumb labels for branch paths, single-branch and multi-branch drill-down scenarios. Uses `S11A_BRANCHES` constant extracted from synthetic node data for stable test assertions. + +### Supporting Changes + +**`syntheticNodes.ts`:** Added optional `uuid` parameter to `makeModelEventNode()`. Set UUIDs on fork-point events in S11a/S11b so branch markers resolve correctly. Added Explore span to S3 for more realistic timeline proportions. + +**`utils/format.ts`:** `formatDuration(start, end)` extracted as a shared utility (was previously defined locally in `TimelineBreadcrumb.tsx`). Takes two `Date` objects and returns a human-readable duration string. + + diff --git a/design/timeline-scanning.md b/design/timeline-scanning.md new file mode 100644 index 000000000..95724a708 --- /dev/null +++ b/design/timeline-scanning.md @@ -0,0 +1,1265 @@ +# Timeline Scanning + +## 1. Universal Message Numbering + +### Problem + +The existing `messages_as_str()` function (`_scanner/extract.py`) numbers messages sequentially starting at `M1`. When scanning a timeline, we scan multiple spans — each span has its own `ModelEvent`s with `input` messages. If each span independently starts numbering at `M1`, references conflict: an LLM citing `[M3]` in one span's scan could collide with `[M3]` from another span. + +### Current Behavior + +```python +messages_str, extract_references = await messages_as_str( + transcript, + preprocessor=preprocessor, + include_ids=True, +) +``` + +- Input: `Transcript` or `list[ChatMessage]` +- Numbering: `M1`, `M2`, `M3`, ... (sequential, 1-indexed) +- Returns: `(formatted_str, extract_references_fn)` +- The `extract_references` closure captures an `id_map: dict[str, str]` mapping ordinals like `"M1"` to actual message IDs + +### Requirements for Timeline Scanning + +When scanning a timeline with spans like `Explore → Plan → Build`: + +1. Each span is scanned independently — its messages are formatted and sent to an LLM +2. References must be globally unique across all span scans within a single timeline scan +3. The `extract_references` function must resolve any citation from any span scan +4. A unified `id_map` must accumulate across all span scans + +### Proposed Design + +A factory function `message_numbering()` returns a pair of functions — a `messages_as_str` that auto-numbers with globally unique IDs, and an `extract_references` that resolves citations across all calls. The numbering state is hidden in the closure: + +```python +MessagesAsStr: TypeAlias = Callable[[list[ChatMessage]], Awaitable[str]] +ExtractReferences: TypeAlias = Callable[[str], list[Reference]] + +def message_numbering( + preprocessor: MessagesPreprocessor | None = None, +) -> tuple[MessagesAsStr, ExtractReferences]: + """Create a messages_as_str / extract_references pair with shared numbering. + + Args: + preprocessor: Message preprocessing options applied to every call + (e.g., exclude_system, exclude_reasoning). Defaults to excluding + system messages. + + Returns: + Tuple of: + - messages_as_str: takes list[ChatMessage], returns formatted string + with globally unique [M1], [M2], etc. + - extract_references: resolves citations from any prior messages_as_str call + """ + ... +``` + +Usage across spans: + +```python +messages_as_str, extract_references = message_numbering( + preprocessor=MessagesPreprocessor(exclude_reasoning=True) +) + +# Span 1: Explore — messages get M1..M12 +explore_str = await messages_as_str(explore_messages) + +# Span 2: Plan — messages continue at M13..M20 +plan_str = await messages_as_str(plan_messages) + +# Span 3: Build — messages continue at M21..M45 +build_str = await messages_as_str(build_messages) + +# Any citation from any span scan resolves correctly +refs = extract_references("See [M14] and [M35]") +``` + +### Changes to `messages_as_str()` + +The existing `messages_as_str()` function is unchanged. The `message_numbering()` factory returns a wrapper that: +- Delegates to the same formatting logic (`message_as_str()` per message, same preprocessor support) +- Uses a closure-captured counter instead of `f"M{len(id_map) + 1}"` +- Accumulates into a closure-captured `id_map` +- Always includes IDs (that's the purpose of creating the numbering scope) + +The standalone `messages_as_str(..., include_ids=True)` continues to work for non-timeline scanners. + +## 2. Composable Message Extraction + +### Data Model Context + +A `TimelineSpan.content` is a list of `TimelineEvent | TimelineSpan`. Each `TimelineEvent` wraps an `Event` — which may be a `ModelEvent`, `ToolEvent`, `CompactionEvent`, etc. + +A `ModelEvent` contains: +- `input: list[ChatMessage]` — the full conversation history at that point +- `output.choices[0].message: ChatMessageAssistant` — the model's response + +For scanning, we need to extract the "conversation" from events. The natural representation is the **last `ModelEvent`'s input + output** — this captures the complete conversation the agent had. But compaction boundaries complicate this: a `CompactionEvent` means the context was compressed, so messages before and after compaction represent different conversational states. + +### Architecture + +The message extraction pipeline is composed of four layers, each independently useful: + +1. **`messages_by_compaction()`** — splits events into message lists at compaction boundaries +2. **`chunked_messages()`** — renders and chunks message lists to fit a context window +3. **`timeline_messages()`** — walks a timeline tree, delegates to `chunked_messages()` per span +4. **`transcript_messages()`** — adaptive dispatch based on available transcript data + +### File Locations + +| Component | Module | +|-----------|--------| +| `messages_by_compaction()`, `chunked_messages()`, `transcript_messages()`, `RenderedMessages` | `_transcript/messages.py` (new) | +| `timeline_messages()`, `TimelineMessages` | `_transcript/timeline.py` (existing) | +| `message_numbering()`, `MessagesAsStr` | `_scanner/extract.py` (existing) | +| `parse_answer()`, `generate_for_answer()`, `generate_answer()` | `_llm_scanner/generate.py` (new) | + +### Event Extraction from Spans + +`timeline_messages()` needs to extract a flat `list[Event]` from a `TimelineSpan` to pass to `chunked_messages()`. A span's `content` is `list[TimelineContentItem]` where `TimelineContentItem = TimelineEvent | TimelineSpan`. To get the span's direct events: + +```python +events = [item.event for item in span.content if isinstance(item, TimelineEvent)] +``` + +Only direct events are extracted — child `TimelineSpan`s are visited recursively by the tree walk, not flattened into the parent's event list. + +### Layer 1: Compaction Splitting + +A pure function that extracts message lists from events, splitting at compaction boundaries: + +```python +def messages_by_compaction(events: list[Event]) -> list[list[ChatMessage]]: + """Split events into message lists at compaction boundaries. + + Filters for ModelEvent and CompactionEvent, then splits based on + compaction type: + - Summary: full split — each segment uses its last ModelEvent's + input + output + - Trim: prefix split — yields trimmed prefix (dropped messages) + as a separate segment, followed by the post-compaction segment + - Edit: ignored — no split + + Args: + events: Events to process (non-Model/Compaction events are + ignored). + + Returns: + List of message lists, one per segment. Empty segments + (no ModelEvents before a compaction boundary, or empty trim + prefix) are omitted. + """ + ... +``` + +#### Compaction Splitting Strategy + +`CompactionEvent` has a `type` field distinguishing the compaction strategy: + +| Compaction Type | Split? | Rationale | +|-----------------|--------|-----------| +| **Summary** | Yes — full split | Original messages replaced by a summary — content is lost. Pre-compaction must be scanned separately. | +| **Edit** | No | Same messages survive with reasoning/tool calls stripped. No content loss, no duplication. | +| **Trim** | Yes — prefix only | Later messages are preserved unchanged, so a full split would create duplication. Instead, yield only the **trimmed prefix** (messages dropped from the beginning) as a separate segment. | + +#### Message Extraction per Segment + +For each segment between compaction boundaries: + +1. Collect `ModelEvent`s and `CompactionEvent`s from the input (ignoring other event types) +2. Split at compaction boundaries by type (summary, trim; edit ignored) +3. For each segment, take the **last `ModelEvent`** — its `input + [output.choices[0].message]` is the conversation for that segment + +``` +Events: [Model₁, Model₂, CompactionSummary, Model₃, Model₄] + └──── segment 0 ────┘ └── segment 1 ──┘ + ↓ ↓ + Model₂.input + output Model₄.input + output +``` + +Trim compaction yields a prefix-only segment: + +``` +Events: [Model₁, Model₂, CompactionTrim, Model₃, Model₄] + + Model₂.input = [A, B, C, D, E, F, G, H] (pre-trim, full conversation) + Model₄.input = [D, E, F, G, H, I] (post-trim, trimmed conversation) + + segment 0: [A, B, C] (trimmed prefix only) + segment 1: Model₄.input + output (post-trim conversation) +``` + +#### Trim Prefix Extraction + +Trim drops messages from the beginning of the conversation. The post-compaction `ModelEvent.input` is a strict suffix of the pre-compaction input. We compute the trimmed prefix to avoid scanning duplicate messages: + +``` +Pre-compaction input: [A, B, C, D, E, F, G, H] +Post-compaction input: [D, E, F, G, H, I, J] + └─────┘ + trimmed prefix → segment 0 (messages lost by trim) + └──────────────┘ + post-compaction → segment 1 (no duplication) +``` + +The prefix is computed by finding the first message in the post-compaction input within the pre-compaction input. Messages use their `id` field for matching when available; when IDs are absent, fall back to index-based alignment (find the longest common suffix of pre-compaction that matches the prefix of post-compaction by content equality). All pre-compaction messages before the match point form the trimmed prefix segment. + +If the prefix is empty (trim removed no messages, or the inputs can't be aligned), no prefix segment is yielded. + +### Layer 2: Chunked Messages + +An async generator that renders and chunks messages to fit a context window. Accepts either a plain message list or a list of events (in which case it delegates to `messages_by_compaction()` internally): + +```python +async def chunked_messages( + source: list[ChatMessage] | list[Event], + *, + messages_as_str: MessagesAsStr, + model: Model, + context_window: int | None = None, +) -> AsyncIterator[RenderedMessages]: + """Render and chunk messages to fit a model's context window. + + When given a list of events, delegates to messages_by_compaction() + to handle compaction boundaries before chunking. When given a + plain message list, treats it as a single segment. + + Each yielded item includes the pre-rendered text (via messages_as_str) + for direct use in scanning. + + Args: + source: Either a list of ChatMessages (single segment) or a + list of Events (split at compaction boundaries, then chunked). + messages_as_str: Rendering function from message_numbering() that + formats messages with globally unique IDs. + model: The model used for scanning. Provides count_tokens() for + measuring rendered text, and max_tokens() for default context + window lookup. + context_window: Override for the model's context window size + (in tokens). When None, looked up via get_model_info(). + An 80% discount factor is applied to leave room for system + prompts and scanning overhead. + + Yields: + RenderedMessages for each segment/chunk. Empty segments are + skipped. + """ + ... +``` + +#### `RenderedMessages` Type + +```python +@dataclass(frozen=True) +class RenderedMessages: + """A chunk of messages, pre-rendered and sized to fit a context window.""" + messages: list[ChatMessage] + text: str # pre-rendered string from messages_as_str + segment: int # 0-based segment index +``` + +Segments result from either compaction boundaries (when given events) or context window chunking — `RenderedMessages` does not distinguish the cause. The `segment` index is 0-based and auto-increments across yields. No total count is tracked — async generators yield lazily and the total isn't known upfront. + +#### Budget Calculation + +```python +from inspect_ai.model._model_info import get_model_info + +DEFAULT_CONTEXT_WINDOW = 128_000 + +if context_window is not None: + budget_base = context_window +else: + info = get_model_info(model) + budget_base = (info.input_tokens() if info else None) or DEFAULT_CONTEXT_WINDOW + +effective_budget = int(budget_base * 0.8) +``` + +Context window lookup uses `get_model_info(model).input_tokens()` — the same approach used by `inspect_ai`'s compaction system. The 80% discount factor reserves headroom for system prompts, scanning instructions, and other overhead that the scanning LLM needs alongside the conversation text. + +#### Chunking Algorithm + +When a segment's messages might exceed the budget, we find chunk boundaries first using `model.count_tokens()` on the raw messages, then render each chunk via `messages_as_str()`: + +1. Use `model.count_tokens(messages)` (which accepts `list[ChatMessage]`) to estimate the full segment's token count +2. If within budget, render the entire segment as one chunk via `messages_as_str()` +3. If over budget, find chunk boundaries by counting tokens on progressively larger prefixes of messages +4. Render each chunk via `messages_as_str()` — the `message_numbering()` counter auto-increments across calls, so numbering is continuous + +``` +Segment messages: [A, B, C, D, E, F, G, H] (too large for context window) + + chunk 0: messages_as_str([A, B, C]) → M1..M3, fits in budget + chunk 1: messages_as_str([D, E, F]) → M4..M6, fits in budget + chunk 2: messages_as_str([G, H]) → M7..M8, fits in budget +``` + +Note: token counting on raw messages is an approximation (the rendered format adds IDs and formatting), but this is acceptable since the 80% discount factor provides ample headroom. The rendered text will always be somewhat larger than raw messages, but well within the 20% margin. + +#### Interaction with Compaction Splitting + +When given events, compaction splitting happens first, then each resulting segment is independently checked against the context window budget. Events with one summary compaction and one oversized post-compaction segment might yield: + +``` +segment 0: pre-compaction messages (fits → 1 chunk) +segment 1a: post-compaction chunk 1 (oversized → split) +segment 1b: post-compaction chunk 2 +``` + +Total: 3 segments numbered 0, 1, 2. The chunking is transparent — callers see a flat sequence of segments. + +#### Standalone Usage + +```python +messages_as_str, extract_references = message_numbering( + preprocessor=MessagesPreprocessor(exclude_reasoning=True) +) + +# With plain messages (no compaction) +async for chunk in chunked_messages( + my_messages, + messages_as_str=messages_as_str, + model=model, +): + # chunk.text is pre-rendered, fits in context window + result = await scan_with_llm(chunk.text) + +# With events (compaction handled automatically) +async for chunk in chunked_messages( + transcript.events, + messages_as_str=messages_as_str, + model=model, +): + result = await scan_with_llm(chunk.text) +``` + +### Layer 3: Timeline Messages + +A high-level async generator that walks a timeline tree, extracts events per span, and delegates to `chunked_messages()`: + +```python +async def timeline_messages( + timeline: Timeline | TimelineSpan, + *, + messages_as_str: MessagesAsStr, + model: Model, + context_window: int | None = None, +) -> AsyncIterator[TimelineMessages]: + """Yield pre-rendered message segments from timeline spans. + + Walks the span tree, extracts events from each non-utility span + with direct ModelEvents, and delegates to chunked_messages() for + compaction splitting and context window chunking. Each yielded + item includes the span context alongside the pre-rendered text. + + To filter which spans are processed, use timeline_filter() before + calling this function. + + Args: + timeline: The timeline (or a specific span subtree) to extract + messages from. If a Timeline, starts from timeline.root. + messages_as_str: Rendering function from message_numbering() that + formats messages with globally unique IDs. + model: The model used for scanning. Provides count_tokens() for + measuring rendered text, and max_tokens() for default context + window lookup. + context_window: Override for the model's context window size + (in tokens). When None, looked up via get_model_info(). + An 80% discount factor is applied to leave room for system + prompts and scanning overhead. + + Yields: + TimelineMessages for each segment. Empty spans are skipped. + """ + ... +``` + +#### `TimelineMessages` Type + +Extends `RenderedMessages` with span context: + +```python +@dataclass(frozen=True) +class TimelineMessages(RenderedMessages): + """A chunk of messages from a specific timeline span.""" + span: TimelineSpan +``` + +Since `TimelineMessages` inherits from `RenderedMessages`, it can be used anywhere a `RenderedMessages` is expected. `timeline_messages()` constructs these from the `RenderedMessages` yielded by `chunked_messages()`, adding the span context for each. + +#### Built-in Filtering + +`timeline_messages()` always applies two built-in filters: + +- **Utility spans** are skipped (single-turn agents with different system prompts). +- **Empty container spans** (no direct `ModelEvent` in their content) are skipped — their children are still visited. + +#### Pre-filtering with `timeline_filter()` + +To select specific spans, use `timeline_filter()` to prune the tree before passing it to `timeline_messages()`: + +```python +def timeline_filter( + timeline: Timeline, + predicate: Callable[[TimelineSpan], bool], +) -> Timeline: +``` + +Non-matching spans and their entire subtrees are removed. The built-in utility and ModelEvent checks in `timeline_messages()` still apply after filtering. + +#### Usage + +```python +messages_as_str, extract_references = message_numbering( + preprocessor=MessagesPreprocessor(exclude_reasoning=True) +) + +# Filter to only "Build" spans +filtered = timeline_filter(timeline, lambda s: s.name == "Build") + +segments: list[TimelineMessages] = [] + +async for span_msgs in timeline_messages( + filtered, + messages_as_str=messages_as_str, + model=model, +): + # span_msgs.text is already pre-rendered with unique message IDs + segments.append(span_msgs) + +# Each segment's .text is ready for the scanning LLM +# All references resolve across all segments +refs = extract_references(llm_output) +``` + +### Layer 4: Transcript Messages + +A convenience generator that encapsulates the "do the right thing" logic for scanning a `Transcript`. It checks what data is available and delegates to the appropriate lower layer: + +```python +async def transcript_messages( + transcript: Transcript, + *, + messages_as_str: MessagesAsStr, + model: Model, + context_window: int | None = None, +) -> AsyncIterator[RenderedMessages]: + """Yield pre-rendered message segments from a transcript. + + Automatically selects the best extraction strategy based on + what data is available on the transcript: + - If timelines are present, delegates to timeline_messages() + - If events are present (no timelines), delegates to + chunked_messages() with compaction handling + - If only messages are present, delegates to chunked_messages() + for context window chunking only + + Since TimelineMessages inherits from RenderedMessages, callers + get a uniform interface. Those needing span context can + isinstance-check for TimelineMessages. + + Args: + transcript: The transcript to extract messages from. + messages_as_str: Rendering function from message_numbering(). + model: The model used for scanning. + context_window: Override for the model's context window size. + + Yields: + RenderedMessages (or TimelineMessages subclass) for each + segment. + """ + ... +``` + +#### Implementation + +```python +async def transcript_messages( + transcript: Transcript, + *, + messages_as_str: MessagesAsStr, + model: Model, + context_window: int | None = None, +) -> AsyncIterator[RenderedMessages]: + if transcript.timelines: + async for seg in timeline_messages( + transcript.timelines[0], + messages_as_str=messages_as_str, + model=model, + context_window=context_window, + ): + yield seg + elif transcript.events: + async for chunk in chunked_messages( + transcript.events, + messages_as_str=messages_as_str, + model=model, + context_window=context_window, + ): + yield chunk + else: + async for chunk in chunked_messages( + transcript.messages, + messages_as_str=messages_as_str, + model=model, + context_window=context_window, + ): + yield chunk +``` + +#### Full Architecture Summary + +The four layers compose into a clean pipeline: + +| Layer | Function | Input | Output | +|-------|----------|-------|--------| +| 1 | `messages_by_compaction()` | `list[Event]` | `list[list[ChatMessage]]` | +| 2 | `chunked_messages()` | `list[ChatMessage] \| list[Event]` | `RenderedMessages` | +| 3 | `timeline_messages()` | `Timeline \| TimelineSpan` | `TimelineMessages` | +| 4 | `transcript_messages()` | `Transcript` | `RenderedMessages` | + +Each layer is independently useful. Most callers will use Layer 4 (`transcript_messages`). Custom pipelines can drop to lower layers for finer control. + +## 3. Reusable Answer Generation and Parsing + +The current `llm_scanner` mixes two concerns: LLM generation (with refusal retry) and answer parsing (extracting a `Result` from model output). These should be independently reusable for custom scanning pipelines. + +### Current State + +The generation and parsing logic lives inline in `llm_scanner`'s scan function: + +- **Normal answers** (boolean, numeric, string, labels): `generate_retry_refusals()` → `resolved_answer.result_for_answer(output, extract_references, value_to_float)` +- **Structured answers** (`AnswerStructured`): `structured_generate()` with tool-based extraction → `structured_result()` for parsing + +The `Answer` protocol (`_llm_scanner/answer.py`) already provides a clean interface for parsing via `result_for_answer()`, but there's no standalone function to drive generation + parsing without going through `llm_scanner`. + +### Proposed API + +Three functions at increasing levels of convenience: + +#### `parse_answer()` — parsing only + +Extracts a `Result` from a `ModelOutput` using the answer specification. For users who have their own generation logic and just want answer extraction: + +```python +def parse_answer( + output: ModelOutput, + answer: Answer, + extract_references: Callable[[str], list[Reference]], + value_to_float: ValueToFloat | None = None, +) -> Result: + """Parse a model output into a Result using the answer specification. + + Delegates to the Answer's result_for_answer() method, handling the + structured answer null-value case. + + Args: + output: The model's response to parse. + answer: Answer specification controlling parsing (boolean, + numeric, string, labels, or structured). + extract_references: Function to extract [M1]-style references + from the explanation text. + value_to_float: Optional function to convert the parsed value + to a float. + + Returns: + A Result with value, answer, explanation, and references. + """ + ... +``` + +#### `generate_for_answer()` — generation only + +Sends a prompt to an LLM using the appropriate strategy (normal or structured), with refusal retry. For users who want our generation logic but custom result handling: + +```python +async def generate_for_answer( + prompt: str | list[ChatMessage], + answer: Answer, + model: str | Model | None = None, + retry_refusals: int = 3, +) -> ModelOutput: + """Generate a model response appropriate for the answer type. + + For structured answers, uses tool-based extraction with multiple + attempts. For all other types, uses standard generation with + refusal retry. + + Args: + prompt: The scanning prompt (string or message list). + answer: Answer specification — determines whether to use + structured (tool-based) or standard generation. + model: Model to use for generation. + retry_refusals: Number of times to retry on model refusals. + + Returns: + The model's output, ready for parsing via parse_answer(). + """ + ... +``` + +#### `generate_answer()` — generation + parsing + +Convenience function combining both steps: + +```python +async def generate_answer( + prompt: str | list[ChatMessage], + answer: Answer, + extract_references: Callable[[str], list[Reference]], + model: str | Model | None = None, + value_to_float: ValueToFloat | None = None, + retry_refusals: int = 3, +) -> Result: + """Generate a model response and parse it into a Result. + + Combines generate_for_answer() and parse_answer() into a single + call. This is the typical entry point for custom scanning pipelines. + + Args: + prompt: The scanning prompt (string or message list). + answer: Answer specification controlling both generation + strategy and parsing. + extract_references: Function to extract [M1]-style references. + model: Model to use for generation. + value_to_float: Optional function to convert the parsed value. + retry_refusals: Number of times to retry on model refusals. + + Returns: + A Result with value, answer, explanation, and references. + """ + output = await generate_for_answer(prompt, answer, model, retry_refusals) + return parse_answer(output, answer, extract_references, value_to_float) +``` + +### Usage + +Custom scanning pipeline using the layered API: + +```python +messages_as_str_fn, extract_references = message_numbering( + preprocessor=MessagesPreprocessor(exclude_reasoning=True) +) + +answer = answer_from_argument("boolean") + +async for chunk in chunked_messages( + my_messages, + messages_as_str=messages_as_str_fn, + model=model, +): + prompt = f"Here is a conversation:\n{chunk.text}\n\nDid the agent refuse?" + + # Option 1: one-step + result = await generate_answer(prompt, answer, extract_references, model=model) + + # Option 2: custom generation, standard parsing + output = await my_custom_generate(prompt) + result = parse_answer(output, answer, extract_references) +``` + +### Location + +These functions should live in `_llm_scanner/` (alongside the existing `answer.py` and `structured.py`) and be publicly exported. The `Answer` protocol and `answer_from_argument()` factory should also be publicly exported so users can construct answer specs. + +## 4. Refactoring `llm_scanner` + +This section stress-tests the design by showing how `llm_scanner` (`_llm_scanner/_llm_scanner.py`) would be refactored to use the new infrastructure from Sections 1–3. + +### Current Implementation + +`llm_scanner` is decorated with `@scanner(messages="all")` and returns `Scanner[Transcript]`. Its scan flow: + +1. Call `messages_as_str(transcript, preprocessor=preprocessor, include_ids=True)` → rendered string + `extract_references` +2. Render the prompt template with `{{ messages }}` set to the rendered string +3. Send prompt to LLM, parse response: + - `AnswerStructured` → `structured_generate()` with tool-based extraction + - All other answer types → `generate_retry_refusals()` with text parsing +4. Return a single `Result` with references resolved via `extract_references` + +Limitations: +- **No context window awareness** — the full message string is injected regardless of size; if it exceeds the model's context window, the call fails +- **No timeline support** — messages are a flat list with no span structure + +### New Parameters + +Two new parameters on `llm_scanner`: + +**`content: TranscriptContent | None`** — controls what data the scanner requests from the framework. `TranscriptContent` (`_transcript/types.py`) is an existing internal dataclass that should be publicly exported with docstrings: + +```python +@dataclass +class TranscriptContent: + """Content filter controlling what data is loaded onto a Transcript. + + Controls which messages, events, and timeline data the scanner + framework loads. Fields set to None are not loaded; set to "all" + to load all of that type. + + Args: + messages: Message types to load (e.g., ["user", "assistant"], + "all", or None). + events: Event types to load (e.g., ["model", "tool"], "all", + or None). + timeline: Timeline configuration. True or "all" builds a + timeline from all events; a list of event types builds + a filtered timeline. + """ + messages: MessageFilter = field(default=None) + events: EventFilter = field(default=None) + timeline: TimelineFilter = field(default=None) +``` + +When provided, `content` overrides the decorator's content filter. `llm_scanner` sets this on the inner scan function via a `SCANNER_CONTENT_ATTR` attribute — the same pattern used by `SCANNER_NAME_ATTR` for the `name` parameter. The `@scanner()` decorator reads this attribute at factory call time and merges it with its own filters before building `ScannerConfig`: + +```python +# In llm_scanner, after creating the scan function: +if content is not None: + setattr(scan, SCANNER_CONTENT_ATTR, content) +``` + +```python +# In the @scanner() decorator, when building ScannerConfig: +# After computing inferred_messages/events/timeline from decorator args... +if hasattr(scanner_fn, SCANNER_CONTENT_ATTR): + override = getattr(scanner_fn, SCANNER_CONTENT_ATTR) + if override.messages is not None: + inferred_messages = override.messages + if override.events is not None: + inferred_events = override.events + if override.timeline is not None: + inferred_timeline = override.timeline +``` + +Fields set on the override take precedence; fields left as `None` fall through to the decorator's values. So `content=TranscriptContent(timeline=True)` adds timeline support while the decorator's `messages="all"` remains active. + +**`context_window: int | None`** — override for the model's context window size, passed through to `transcript_messages()`. + +### Adaptive Detection + +`llm_scanner` keeps its existing decorator — `@scanner(messages="all")` — as the base config. The `content` parameter extends it at factory call time via `SCANNER_CONTENT_ATTR`. At scan time, `transcript_messages()` (Layer 4) handles the adaptive detection — it checks `transcript.timelines`, then `transcript.events`, then falls back to `transcript.messages`. + +This is fully backward compatible — direct use without `content` behaves exactly as today, now with context window protection. + +### Refactored Implementation + +```python +@scanner(messages="all") +def llm_scanner( + *, + question: str | Callable[[Transcript], Awaitable[str]] | None = None, + answer: Literal["boolean", "numeric", "string"] + | list[str] + | AnswerMultiLabel + | AnswerStructured, + value_to_float: ValueToFloat | None = None, + template: str | None = None, + template_variables: dict[str, Any] + | Callable[[Transcript], dict[str, Any]] + | None = None, + preprocessor: MessagesPreprocessor[Transcript] | None = None, + model: str | Model | None = None, + content: TranscriptContent | None = None, + context_window: int | None = None, + retry_refusals: bool | int = 3, + name: str | None = None, +) -> Scanner[Transcript]: + if template is None: + template = DEFAULT_SCANNER_TEMPLATE + resolved_answer = answer_from_argument(answer) + retry_refusals = ( + retry_refusals if isinstance(retry_refusals, int) + else 3 if retry_refusals is True + else 0 + ) + + # Serialize Model instances for cloudpickle roundtrips + serializable_model: str | ModelConfig | None + if isinstance(model, Model): + serializable_model = model_to_model_config(model) + else: + serializable_model = model + + async def scan(transcript: Transcript) -> Result | list[Result]: + resolved_model: str | Model | None = ( + model_config_to_model(serializable_model) + if isinstance(serializable_model, ModelConfig) + else serializable_model + ) + model_instance = get_model(resolved_model) + + # Shared numbering scope — globally unique message IDs, + # extract_references resolves across all segments + messages_as_str_fn, extract_references = message_numbering( + preprocessor=preprocessor, + ) + + # Scan each segment — transcript_messages() handles + # timelines, events, or plain messages automatically + results: list[Result] = [] + async for segment in transcript_messages( + transcript, + messages_as_str=messages_as_str_fn, + model=model_instance, + context_window=context_window, + ): + prompt = await render_scanner_prompt( + template=template, + template_variables=template_variables, + transcript=transcript, + messages=segment.text, + question=question, + answer=resolved_answer, + ) + + result = await generate_answer( + prompt, + resolved_answer, + extract_references, + model=resolved_model, + value_to_float=value_to_float, + retry_refusals=retry_refusals, + ) + results.append(result) + + # Single segment → single Result (backward compatible) + return results[0] if len(results) == 1 else results + + if content is not None: + setattr(scan, SCANNER_CONTENT_ATTR, content) + if name is not None: + setattr(scan, SCANNER_NAME_ATTR, name) + return scan +``` + +### What Changes, What Doesn't + +| Aspect | Before | After | +|--------|--------|-------| +| Decorator | `@scanner(messages="all")` | unchanged | +| New params | — | `content`, `context_window` | +| Return type | `Result` | `Result \| list[Result]` (single segment still returns `Result`) | +| Message extraction | `messages_as_str(transcript, ...)` | `transcript_messages()` + `message_numbering()` | +| Context window | no protection | 80% budget, auto-chunked | +| Timeline support | none | opt-in via `content=TranscriptContent(timeline=True)` | +| Compaction handling | none | auto via `transcript_messages()` | +| Prompt template | unchanged | works as-is per segment | +| `extract_references` | scoped to one call | shared across all segments | +| Generation + parsing | inline if/else | `generate_answer()` from Section 3 | + +### Usage Examples + +Direct use with timeline support: + +```python +my_scanner = llm_scanner( + question="Did the agent follow a clear plan?", + answer="boolean", + content=TranscriptContent(timeline=True), +) +``` + +Direct use with context window override: + +```python +my_scanner = llm_scanner( + question="Summarize the key decisions.", + answer="string", + context_window=50000, +) +``` + +Delegation from a parent scanner (parent's decorator controls loading): + +```python +@scanner(timeline=True) +def quality_scanner(*, model: str | None = None) -> Scanner[Transcript]: + return llm_scanner( + question="Did the agent follow a clear plan?", + answer="boolean", + model=model, + ) +``` + +### Preprocessor Compatibility + +The current `llm_scanner` uses `MessagesPreprocessor[Transcript]` — a preprocessor that can accept a full `Transcript` for transforms that need transcript context. The `message_numbering()` preprocessor operates on `list[ChatMessage]`. + +Standard preprocessors (`exclude_system`, `exclude_reasoning`) only need the message list, so they work unchanged. Custom transforms that access `Transcript` metadata would need adaptation — transcript-level transforms can be applied before messages enter the pipeline. + +### Public Exports + +`TranscriptContent` needs to be publicly exported (currently internal) with the docstrings shown above. The filter type aliases (`MessageFilter`, `EventFilter`, `TimelineFilter`) should also be exported for users constructing `TranscriptContent` instances. + +## 5. Parallel Generation (Speculative) + +### Motivation + +The refactored `llm_scanner` scan loop is sequential: + +```python +async for segment in transcript_messages(...): + prompt = await render_scanner_prompt(...) + result = await generate_answer(...) # seconds per call + results.append(result) +``` + +For a timeline with 5 spans, each with 2 context window chunks, this means 10 sequential LLM calls. At ~3 seconds each, that's 30 seconds. With parallel generation (bounded to, say, 4 concurrent), the same work completes in ~9 seconds. + +### Why Streaming (Not Collect-Then-Parallelize) + +Both phases of the pipeline involve remote API calls: + +1. **Rendering** — `chunked_messages()` calls `model.count_tokens()` per segment to check budget and find chunk boundaries. Each call hits the LLM provider's token counting API. +2. **Generation** — `generate_answer()` makes LLM generation API calls, each taking seconds. + +A naive "collect all segments, then parallelize generation" approach serializes all the token counting before any generation begins. With streaming, generation on segment 0 can start while `transcript_messages()` is still doing `count_tokens()` calls to render segments 1, 2, 3. The overlap between production (token counting) and consumption (generation) provides real throughput gains. + +Note: rendering must remain sequential — `message_numbering()` has a monotonic counter, so segments must be produced in order. But once a segment is yielded, its generation is independent. + +### Proposed Approach: anyio Memory Streams + +anyio memory streams provide a clean producer-consumer pattern with backpressure: + +``` +transcript_messages() ──[send]──▷ [buffer] ──[receive]──▷ worker 1 → generate_answer() + ──[receive]──▷ worker 2 → generate_answer() + ──[receive]──▷ worker N → generate_answer() +``` + +```python +import anyio +from anyio.streams.memory import MemoryObjectSendStream, MemoryObjectReceiveStream + + +async def parallel_scan( + segments: AsyncIterator[RenderedMessages], + generate_fn: Callable[[RenderedMessages], Awaitable[Result]], + model: Model, +) -> list[Result]: + """Stream segments into parallel generation workers. + + The producer (segments iterator) runs sequentially — it must, because + message numbering requires ordered rendering. Workers consume segments + as they arrive and generate in parallel. Results are reassembled in + segment order. + + Concurrency is derived from the model's max_connections, which is + already configured by the scan infrastructure to match the provider's + rate limits. + + Args: + segments: Async iterator of pre-rendered segments (e.g., from + transcript_messages()). + generate_fn: Async function that generates a Result from a + rendered segment. + model: Model instance — its max_connections determines worker + count and buffer size. + + Returns: + Results in segment order. + """ + max_concurrency = model.config.max_connections or model.api.max_connections() + results: dict[int, Result] = {} + + send_stream, receive_stream = anyio.create_memory_object_stream[ + RenderedMessages + ](max_buffer_size=max_concurrency) + + async def producer() -> None: + async with send_stream: + async for segment in segments: + await send_stream.send(segment) + + async def worker(rx: MemoryObjectReceiveStream[RenderedMessages]) -> None: + async for segment in rx: + results[segment.segment] = await generate_fn(segment) + + async with anyio.create_task_group() as tg: + tg.start_soon(producer) + # Clone the receive stream for each worker; close original + for _ in range(max_concurrency): + tg.start_soon(worker, receive_stream.clone()) + receive_stream.close() + + return [results[i] for i in sorted(results)] +``` + +### Usage in `llm_scanner` + +```python +async def scan(transcript: Transcript) -> Result | list[Result]: + model_instance = get_model(resolved_model) + messages_as_str_fn, extract_references = message_numbering( + preprocessor=preprocessor, + ) + + async def generate_one(segment: RenderedMessages) -> Result: + prompt = await render_scanner_prompt( + template=template, + template_variables=template_variables, + transcript=transcript, + messages=segment.text, + question=question, + answer=resolved_answer, + ) + return await generate_answer( + prompt, + resolved_answer, + extract_references, + model=resolved_model, + value_to_float=value_to_float, + retry_refusals=retry_refusals, + ) + + results = await parallel_scan( + transcript_messages( + transcript, + messages_as_str=messages_as_str_fn, + model=model_instance, + context_window=context_window, + ), + generate_one, + model=model_instance, + ) + + return results[0] if len(results) == 1 else results +``` + +### Concurrency Considerations + +- **Derived from model**: Worker count and buffer size come from `model.config.max_connections or model.api.max_connections()`. The scan infrastructure already sets `max_connections` on the model's `GenerateConfig` (defaulting to `max_transcripts`, typically 25), so this aligns with the provider's actual rate limits without a magic number. +- **Double-layered throttling**: The model's internal connection semaphore (`_connection_concurrency`) provides API-level rate limiting. Our worker count bounds task creation — preventing excessive coroutines waiting on that semaphore. Both layers work together: workers bound the fan-out, the semaphore bounds actual API calls. +- **Cross-transcript sharing**: In a full scan, multiple transcript scans run concurrently, all sharing the same model connection pool. Each transcript's `parallel_scan` creates up to `max_connections` workers, but they all compete for the same semaphore. The model's backpressure naturally distributes throughput across transcripts. +- **Backpressure**: `max_buffer_size=max_concurrency` means the producer blocks when all workers are busy. This prevents unbounded memory growth if rendering is faster than generation (which it always is, except when `count_tokens()` calls queue behind busy connections). +- **Single segment fast path**: When there's only one segment (common case for short conversations), the stream pattern degenerates to a single send/receive with negligible overhead. +- **Error handling**: `anyio.TaskGroup` propagates the first exception and cancels remaining tasks — the right behavior when one generation fails, since we can't produce a complete result set. +- **Ordering**: Results are keyed by `segment.segment` (the 0-based index) and reassembled in order at the end, regardless of completion order. + +## 6. Implementation Phases + +### Working Guidelines + +1. **One phase at a time.** Implement, test, and verify each phase before moving to the next. +2. **Review before commit.** After tests pass, pause and review the code together before committing. Do not auto-commit. +3. **Full tests at each step.** Every phase produces both an implementation file and a test file. +4. **Use synthetic scenarios.** Build minimal inline test helpers (e.g., `make_model_event()`, `make_compaction_event()`) using direct `inspect_ai` constructors. Reuse the existing `_parse_input_messages()` pattern from `tests/transcript/nodes/test_timeline.py`. Use the shared JSON fixtures in `tests/transcript/nodes/fixtures/events/` where they cover relevant scenarios (e.g., `compaction_boundary.json`). +5. **Update this document.** After completing a phase but before committing, replace the phase's overview section below with a summary of what was actually built and tested — files created/modified, key design decisions made during implementation, and test coverage. + +### Phase 1: `message_numbering()` ✓ `563d43aa` + +**Implements:** Section 1 (Universal Message Numbering) + +**Files modified:** +- `src/inspect_scout/_scanner/extract.py` — added `message_numbering()` factory function +- `src/inspect_scout/__init__.py` — added `message_numbering` to public exports +- `tests/scanner/test_extract.py` — added 9 test cases + +**What was built:** +- `message_numbering(preprocessor)` factory that returns a `(messages_as_str, extract_references)` pair with shared closure state (counter + id_map) +- The returned `messages_as_str` takes `list[ChatMessage]`, applies preprocessing, and renders with globally unique `[M1]`, `[M2]`, etc. prefixes using a monotonically incrementing counter +- The returned `extract_references` resolves `[M{n}]` citations from any prior `messages_as_str` call +- `MessagesAsStr` and `ExtractReferences` type aliases for the returned callables +- Existing `messages_as_str()` function unchanged (backward compatible) + +**Test coverage:** +- Single call produces M1..Mn +- Multiple calls continue numbering (M1..M2, then M3..M5) +- `extract_references` resolves citations across all prior calls +- Empty message lists don't advance the counter +- Default excludes system messages +- Preprocessor options: `exclude_reasoning`, `exclude_tool_usage` +- Custom `transform` function +- Filtered messages don't consume numbers across calls + +**Dependencies:** None — standalone. + +### Phase 2: `messages_by_compaction()` ✓ `f38279f4` + +**Implements:** Section 2, Layer 1 (Compaction Splitting) + +**Files created:** +- `src/inspect_scout/_transcript/messages.py` — `messages_by_compaction()`, `_segment_messages()`, `_trim_prefix()` +- `tests/transcript/test_messages.py` — 11 test cases +- `src/inspect_scout/__init__.py` — added `messages_by_compaction` to public exports + +**What was built:** +- `messages_by_compaction(events)` — pure function that filters for `ModelEvent` and `CompactionEvent`, splits by compaction type (summary: full split, trim: prefix-only split, edit: no split) +- Each segment's messages come from the **last** `ModelEvent` in that segment: `input + [output.choices[0].message]` +- `_trim_prefix(pre_input, post_input)` — finds overlap point via message `id` matching, falls back to content equality (`text` + `role`) +- `_segment_messages(model_event)` — extracts `input + output` with edge case handling for missing output + +**Test coverage:** +- No compaction, summary, trim, edit compaction types +- Multiple compactions in sequence +- Empty segments omitted, non-model events ignored +- Trim with empty prefix (all messages survive) +- Trim prefix via id matching +- Empty event list, only compaction events + +**Dependencies:** None — standalone pure function. + +### Phase 3: `chunked_messages()` + `MessagesChunk` ✓ 8b2a3a22 + +**Implements:** Section 2, Layer 2 (Chunked Messages) + +**Files:** +- Modify: `src/inspect_scout/_transcript/messages.py` +- Modify: `tests/transcript/test_messages.py` + +**What to build:** +- `RenderedMessages` dataclass (`messages`, `text`, `segment`) +- `chunked_messages(source, messages_as_str, model, context_window)` async generator +- Accepts `list[ChatMessage]` or `list[Event]` (delegates to `messages_by_compaction()` for events) +- Uses `model.count_tokens()` for budget checking, 80% discount factor +- Yields `RenderedMessages` per segment/chunk + +**What to test:** +- Small message list → single `RenderedMessages` (no chunking) +- Large message list exceeding budget → multiple chunks with continuous numbering +- Events with compaction → compaction split, then chunked +- Budget calculation: 80% discount factor applied correctly +- Empty input yields nothing +- Uses a mock or stub model with controllable `count_tokens()` and context window + +**Dependencies:** Phase 1 (`message_numbering`), Phase 2 (`messages_by_compaction`). + +### Phase 4: `timeline_messages()` + `transcript_messages()` ✓ 3411c21f + +**Implements:** Section 2, Layers 3+4 (Timeline Messages + Transcript Messages) + +**Files:** +- Modify: `src/inspect_scout/_transcript/timeline.py` +- Modify: `src/inspect_scout/_transcript/messages.py` +- Modify: `tests/transcript/test_messages.py` + +**What to build:** +- `TimelineMessages(RenderedMessages)` dataclass — adds `span: TimelineSpan` +- `timeline_messages(timeline, messages_as_str, model, context_window)` async generator +- Walks span tree, extracts `[item.event for item in span.content if isinstance(item, TimelineEvent)]` +- Delegates to `chunked_messages()` per span, wraps results as `TimelineMessages` +- Built-in: skips utility spans and spans without direct ModelEvents +- `timeline_filter(timeline, predicate)` — prunes non-matching spans and subtrees before scanning +- `transcript_messages(transcript, messages_as_str, model, context_window)` async generator +- Adaptive dispatch: `transcript.timelines` → `timeline_messages()`, `transcript.events` → `chunked_messages()`, else `transcript.messages` → `chunked_messages()` + +**What to test:** +- Single-span timeline → yields `TimelineMessages` with correct span context +- Multi-span timeline → yields segments in tree-walk order with continuous numbering +- Nested spans → child spans visited recursively +- Default skips utility spans and empty container spans +- `timeline_filter` prunes by predicate, subtrees removed +- `transcript_messages` dispatches to correct layer based on available data + +**Dependencies:** Phase 3 (`chunked_messages`). + +### Phase 5: Answer Generation Functions ✓ + +**Implements:** Section 3 (Reusable Answer Generation) + +**Files created:** +- `src/inspect_scout/_llm_scanner/generate.py` — `parse_answer()` and `generate_answer()` +- `tests/llm_scanner/test_generate.py` — 18 test cases + +**Files modified:** +- `src/inspect_scout/_llm_scanner/types.py` — added `AnswerSpec` type alias (the answer union type, previously spelled out inline three times in `llm_scanner`) +- `src/inspect_scout/_llm_scanner/_llm_scanner.py` — uses `AnswerSpec` instead of repeating the union; cleaned up unused imports +- `src/inspect_scout/_llm_scanner/__init__.py` — re-exports new public API +- `src/inspect_scout/__init__.py` — public exports + +**What was built:** + +The design doc proposed three separate functions (`parse_answer`, `generate_for_answer`, `generate_answer`). The implementation simplified this to two, combining generation and optional parsing into a single overloaded `generate_answer` function: + +- `parse_answer(output, answer, extract_references, value_to_float)` → `Result` — pure parsing, no LLM call. Accepts `AnswerSpec`, resolves internally via `answer_from_argument()`. + +- `generate_answer(prompt, answer, *, model, retry_refusals, parse, extract_references, value_to_float)` → `Result | ModelOutput` — overloaded on `parse`: + - `parse=True` (default): generates then parses → returns `Result` + - `parse=False`: generation only → returns `ModelOutput` + - Dispatches structured answers to `structured_generate()`, normal answers to `generate_retry_refusals()` + - Handles the structured null-value case (`value=None` → `Result(value=None, answer=completion)`) + - `extract_references` defaults to a no-op (no references), so callers who don't need references don't pass a dummy function + +- `AnswerSpec` type alias in `types.py` — defines the answer union once, used by both `llm_scanner` and `generate_answer` + +**Design decisions:** +- `generate_for_answer` was merged into `generate_answer` with `parse=False` instead of being a separate function — reduces API surface without losing capability +- `Answer` protocol and `answer_from_argument()` remain internal — external users construct `AnswerSpec` values directly; resolution happens inside the functions +- Parameter ordering: generation params (`model`, `retry_refusals`) before parsing params (`parse`, `extract_references`, `value_to_float`) + +**Test coverage:** +- `parse_answer`: boolean yes/no, numeric integer/decimal, string text/no-pattern, labels single/invalid, references extraction, value_to_float, structured +- `generate_answer`: normal dispatch, structured dispatch, parse=False returns ModelOutput, parse=True returns Result, extract_references used/default, structured null value + +**Dependencies:** None — independent of Phases 2-4. + +### Phase 6: Refactor `llm_scanner` (COMPLETE) + +**Implements:** Section 4 (Refactoring) + +**What was built:** + +1. **`SCANNER_CONTENT_ATTR`** in `src/inspect_scout/_scanner/scanner.py` — new constant + and merge logic in `factory_wrapper` that reads the attribute from scan functions + and overrides inferred content filters. Inserted after filter inference but before + validation so overridden filters are validated and included in `ScannerConfig`. + +2. **Refactored `_llm_scanner.py`:** + - Added `content: TranscriptContent | None`, `context_window: int | None`, + `compaction: Literal["all", "last"]` (default `"all"`), and `depth: int | None` + parameters to all three signatures (two overloads + implementation) + - Replaced `messages_as_str(transcript, ...)` with `message_numbering()` + + `transcript_messages()` for context-window-aware, timeline-capable extraction + - Replaced inline `structured_generate`/`generate_retry_refusals` with `generate_answer()` + - Return type changed from `Result` to `Result | list[Result]` + - Preprocessor passed through with `cast()` for backward compat — `MessageFormatOptions` + fields work identically; custom `transform` taking `Transcript` is a documented limitation + - `content=` sets `SCANNER_CONTENT_ATTR` on the scan function for the decorator to merge + - `compaction` and `depth` passed through to `transcript_messages()` — `compaction` + controls how compaction boundaries in events are handled; `depth` limits the span + tree depth when timelines are present + +3. **`_flatten_results()` helper** — prevents nested resultsets when structured answers + with `result_set=True` are used across multiple segments. Checks each result for + `type=="resultset"` with list value, deserializes sub-results via `Result.model_validate`, + and collects into a flat list. + +4. **`TranscriptContent` exported** from `src/inspect_scout/__init__.py`. + +**Tests:** 12 new tests in `tests/llm_scanner/test_llm_scanner.py`: +- `_flatten_results`: passthrough, unrolling, mixed, edge cases (list value non-resultset, + resultset type non-list value), metadata preservation +- `content` parameter: sets attr, no attr without content, events filter + +All 157 llm_scanner tests pass (145 existing + 12 new). Full suite: 2404 passed. + +**Dependencies:** All prior phases (1-5). + +### Phase 7: Parallel Generation + +**Implements:** Section 5 (Parallel Generation) + +**Files:** +- Modify: `src/inspect_scout/_llm_scanner/_llm_scanner.py` (or new utility module) +- Modify: tests for `llm_scanner` + +**What to build:** +- `parallel_scan(segments, generate_fn, model)` using anyio memory streams +- Producer: sequential `transcript_messages()` iterator (numbering must stay ordered) +- Workers: `max_concurrency` parallel consumers calling `generate_answer()` +- Concurrency derived from `model.config.max_connections` or `model.api.max_connections()` +- Results reassembled in segment order via `segment.segment` index +- Backpressure via `max_buffer_size=max_concurrency` + +**What to test:** +- Single segment → degenerates to single send/receive (no overhead) +- Multiple segments → results returned in order regardless of completion order +- Worker count matches model's `max_connections` +- Error in one generation cancels remaining tasks (anyio TaskGroup behavior) +- Backpressure: producer blocks when buffer is full + +**Dependencies:** Phase 6 (`llm_scanner` refactor). diff --git a/docs/_custom_scanner.md b/docs/_custom_scanner.md new file mode 100644 index 000000000..4cabdafba --- /dev/null +++ b/docs/_custom_scanner.md @@ -0,0 +1,62 @@ + +If the higher level LLM and Grep scanners don't meet your requirements, you can write a custom scanners with whatever behavior you need. For example, the following illustrates the implementation of a basic LLM scanner (click on the numbers at right for further explanation): + +``` {.python filename="scanner.py"} +from inspect_scout import ( + Result, Scanner, Transcript, scanner, message_numbering +) + +from inspect_ai.model import get_model +import re + +@scanner(messages="all") # <1> +def ctf_environment() -> Scanner[Transcript]: + + async def scan(transcript: Transcript) -> Result: + + # setup message numbering + messages_as_str, extract_refs = message_numbering() + + # call model + output = await get_model().generate( # <2> + "Here is a transcript of an LLM agent " + + "solving a cybersecurity challenge:\n\n" + + "===================================" + # <3> + await messages_as_str(transcript.messages) + # <3> + "===================================\n\n" + # <3> + "In the transcript above do you detect any " + + "instances of environment misconfiguration " + + "preventing the agent from completing its " + + "task? If no, simply respond with 'No'. " + + "If yes, respond with 'Yes' followed by an " + + "explanation." + ) + + # extract the first word + match = re.match(r"^\w+", output.completion.strip()) + + # return result + if match: + answer = match.group(0) + explanation = output.completion + return Result( # <4> + value=answer.lower() == "yes", # <4> + answer=answer, # <4> + explanation=explanation, # <4> + references=extract_refs(explanation) # <4> + ) # <4> + else: + return Result(value=False, explanation=output.completion) + + return scan +``` + +1. Scanners are decorated with `@scanner` so they can specify the exact subset of content they need to read. In this case only messages (and not events) will be read from the log, decreasing load time. + +2. Scanners frequently use models to perform scanning. Calling `get_model()` utilizes the default model for the scan job (which can be specified in the top level call to scan). + +3. Convert the message history into a string for presentation to the model. The `messages_as_str()` function takes a `Transcript | list[Messages]` and will by default remove system messages from the message list. See `MessagesPreprocessor` for other available options. + +4. As with scorers, results also include additional context (here the extracted answer, full model completion, and message references). + +For more details on creating custom scanners, including scanning individual messages or events, handling compaction and context overflow, and computing metrics, see the article on [Custom Scanners](custom_scanner.qmd). \ No newline at end of file diff --git a/docs/_grep_scanner.md b/docs/_grep_scanner.md new file mode 100644 index 000000000..95a9636f1 --- /dev/null +++ b/docs/_grep_scanner.md @@ -0,0 +1,12 @@ + +Using an LLM to search transcripts is often required for more nuanced judgements, but if you are just looking for text patterns, you can also use the `grep_scanner()`. For example, here we search assistant messages for references to phrases that might indicate secrets: + +```python +from inspect_scout import Transcript, grep_scanner, scanner + +@scanner(messages=["assistant"]) +def secrets() -> Scanner[Transcript]: + return grep_scanner(["password", "secret", "token"]) +``` + +For additional details on using this scanner, see the [Grep Scanner](grep_scanner.qmd) article. diff --git a/docs/_llm_scanner.md b/docs/_llm_scanner.md new file mode 100644 index 000000000..c57a97082 --- /dev/null +++ b/docs/_llm_scanner.md @@ -0,0 +1,17 @@ + +For many applications you can use the high-level `llm_scanner()`, which uses a model for transcript analysis and can be customized with many options. For example: + +``` {.python filename="scanner.py"} +from inspect_scout import Scanner, Transcript, llm_scanner, scanner + +@scanner(messages="all") +def ctf_environment() -> Scanner[Transcript]: + return llm_scanner( + question="In the transcript above do you detect " + "instances of environment misconfiguration " + "preventing the agent from completing it's task?", + answer="boolean" + ) +``` + +The `llm_scanner()` supports a wide variety of model answer types including boolean, number, string, classification (single or multi), and structured JSON output. For additional details, see the [LLM Scanner](llm_scanner.qmd) article. \ No newline at end of file diff --git a/docs/_quarto.yml b/docs/_quarto.yml index a0ef97cd1..a9354727a 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -36,17 +36,21 @@ website: left: - text: "Getting Started" href: index.qmd - - href: examples.qmd - text: "Using" menu: + - href: scanners.qmd + - href: transcripts.qmd - href: workflow.qmd - href: projects.qmd - - href: transcripts.qmd - - href: scanners.qmd - - href: llm_scanner.qmd - - href: grep_scanner.qmd - href: results.qmd - href: validation.qmd + - text: "Scanners" + menu: + - href: llm_scanner.qmd + - href: grep_scanner.qmd + - href: custom_scanner.qmd + - href: scanner_tools.qmd + - href: multi_agent.qmd - text: "Transcripts" menu: - href: db_overview.qmd @@ -78,15 +82,20 @@ website: - text: Using Scout href: workflow.qmd contents: + - href: scanners.qmd + - href: transcripts.qmd - href: workflow.qmd - href: projects.qmd - - href: transcripts.qmd - - href: scanners.qmd - - href: llm_scanner.qmd - - href: grep_scanner.qmd - href: results.qmd - href: validation.qmd - - text: Transcripts Database + - text: "Scanners" + contents: + - href: llm_scanner.qmd + - href: grep_scanner.qmd + - href: custom_scanner.qmd + - href: scanner_tools.qmd + - href: multi_agent.qmd + - text: Transcripts href: db_overview.qmd contents: - text: Overview diff --git a/docs/_scan_jobs.md b/docs/_scan_jobs.md new file mode 100644 index 000000000..4058ebf76 --- /dev/null +++ b/docs/_scan_jobs.md @@ -0,0 +1,44 @@ + +You may want to import scanners from other modules and compose them into a `ScanJob`. To do this, add a `@scanjob` decorated function to your source file (it will be used in preference to `@scanner` decorated functions). + +A `ScanJob` can also include `transcripts` or any other option that you can pass to `scout scan` (e.g. `model`). For example: + +``` {.python filename="scanning.py"} +from inspect_scout import ScanJob, scanjob + +@scanjob +def job() -> ScanJob: + return ScanJob( + scanners=[ctf_environment(), java_tool_usages()], + transcripts="./logs", + model="openai/gpt-5" + ) +``` + +You can then use the same command to run the job (`scout scan` will prefer a `@scanjob` defined in a file to individual scanners): + +``` bash +scout scan scanning.py +``` + +You can also specify a scan job using YAML or JSON. For example, the following is equivalent to the example above: + +``` {.yaml filename="scan.yaml"} +scanners: + - name: deception + file: scanner.py + - name: java_tool_usages + file: scanner.py + +transcripts: logs +filter: task_set='cybench' + +model: openai/gpt-5 +``` + +Which can be executed with: + +``` bash +scout scan scan.yaml +``` + diff --git a/docs/_transcript_fields.md b/docs/_transcript_fields.md index aa9ac0c9b..ed8c80bd4 100644 --- a/docs/_transcript_fields.md +++ b/docs/_transcript_fields.md @@ -24,6 +24,7 @@ Here are the available `Transcript` fields: | `metadata` | dict\[str, JsonValue\] | Transcript source specific metadata (e.g. model, task name, errors, epoch, dataset sample id, limits, etc.). | | `messages` | [list\[ChatMessage\]](https://inspect.aisi.org.uk/reference/inspect_ai.model.html#messages) | Message history. | | `events` | [list\[Event\]](https://inspect.aisi.org.uk/reference/inspect_ai.event.html) | Event history (e.g. model events, tool events, etc.) | +| `timelines` | list\[Timeline\] | Optional list of custom timelines for this transcript. | : {tbl-colwidths=\[20,30,50\]} diff --git a/docs/custom_scanner.qmd b/docs/custom_scanner.qmd new file mode 100644 index 000000000..f58333cff --- /dev/null +++ b/docs/custom_scanner.qmd @@ -0,0 +1,454 @@ +--- +title: Custom Scanners +--- + +## Overview + +Scanners are the main unit of processing in Inspect Scout and can target a wide variety of content types. In this article we'll cover the basic scanning concepts, and then drill into creating scanners that target various types (`Transcript`, `ChatMessage`, `Event`, or `Timeline`) as well as creating custom loaders which enable scanning of lists of events or messages. + +This article goes in depth on custom scanner development. If you are looking for a straightforward high-level way to create an LLM-based scanner see the [LLM Scanner](llm_scanner.qmd) documentation. + +Note that you can also use scanners directly as Inspect scorers (see [Scanners as Scorers](#scanners-as-scorers) for details). + +## Scanner Basics + +A `Scanner` is a function that takes a `ScannerInput` (typically a `Transcript`, but possibly an `Event`, `ChatMessage`, `Timeline`, or list of events or messages) and returns a `Result`. + +The result includes a `value` which can be of any type---this might be `True` to indicate that something was found but might equally be a number to indicate a count. More elaborate scanner values (`dict` or `list`) are also possible. + +Here is a simple scanner that uses a model to look for agent "confusion"---whether or not it finds confusion, it still returns the model completion as an `explanation`: + +``` python +from inspect_scout import ( + Result, Scanner, Transcript, scanner, message_numbering +) + +from inspect_ai.model import get_model +import re + +@scanner(messages="all") +def confusion() -> Scanner[Transcript]: + + async def scan(transcript: Transcript) -> Result: + + # setup message numbering + messages_as_str, extract_refs = message_numbering() + + # call model + output = await get_model().generate( + "Here is a transcript of an LLM agent " + + "solving a puzzle:\n\n" + + "===================================" + + await messages_as_str(transcript.messages) + + "===================================\n\n" + + "In the transcript above do you see the " + + "agent becoming confused? Respond " + + "beginning with 'Yes' or 'No', followed " + + "by an explanation." + ) + + # extract the first word + match = re.match(r"^\w+", output.completion.strip()) + + # return result + if match: + answer = match.group(0) + explanation = output.completion + return Result( + value=answer.lower() == "yes", + answer=answer, + explanation=explanation, + references=extract_refs(explanation), + ) + else: + return Result(value=False, explanation=output.completion) + + return scan +``` + +This scanner illustrates some of the lower-level mechanics of building custom scanners. You can also use the higher level `llm_scanner()` to implement this in far fewer lines of code: + +``` python +from inspect_scout import Transcript, llm_scanner, scanner + +@scanner(messages="all") +def confusion() -> Scanner[Transcript]: + return llm_scanner( + question="In the transcript above do you see " + + "the agent becoming confused?" + answer="boolean" + ) +``` + +### Input Types + +`Transcript` is the most common `ScannerInput` however several other types are possible: + +- `Event` --- Single event from the transcript (e.g. `ModelEvent`, `ToolEvent`, etc.). + +- `ChatMessage` --- Single chat message from the transcript message history. + +- `Timeline` --- Hierarchical span tree representing the structure of agent execution. See [Timeline Scanners](#timelines) below for details. + +- `list[Event]` or `list[ChatMessage]` --- Arbitrary sets of events or messages extracted from the `Transcript` (see [Loaders](#loaders) below for details). + +See the sections on [Transcripts](#transcripts), [Events](#events), [Messages](#messages), [Timelines](#timelines), and [Loaders](#loaders) below for additional details on handling various input types. + +### Input Filtering + +One important principle of the Inspect Scout transcript pipeline is that only the precise data to be scanned should be read, and nothing more. This can dramatically improve performance as messages and events that won't be seen by scanners are never deserialized. Scanner input filters are specified as arguments to the `@scanner` decorator (you may have noticed the `messages="all"` attached to the scanner decorator in the example above). + +For example, here we are looking for instances of assistants swearing---for this task we only need to look at assistant messages so we specify `messages=["assistant"]` + +``` python +@scanner(messages=["assistant"]) +def assistant_swearing() -> Scanner[Transcript]: + + async def scan(transcript: Transcript) -> Result: + swear_words = [ + word + for m in transcript.messages + for word in extract_swear_words(m.text) + ] + return Result( + value=len(swear_words), + explanation=",".join(swear_words) + ) + + return scan +``` + +With this filter, only assistant messages (and no events at all) will be loaded from transcripts during scanning. + +Note that by default, no filters are active, so if you don't specify values for `messages`, `events`, and/or `timeline` your scanner will not be called! + +The available filter parameters are: + +- `messages` --- Filter for message types: `"all"` or a list like `["user", "assistant"]`. +- `events` --- Filter for event types: `"all"` or a list like `["model", "tool"]`. +- `timeline` --- Enable timeline loading: `True` for all event types, `"all"`, or a list like `["model", "tool"]`. + +You can also provide a `version` parameter to track scanner versions. When the version is incremented, previously scanned transcripts will be re-scanned with the updated scanner. + +## Transcripts {#transcripts} + +Transcripts are the most common input to scanners. If you are reading from Inspect eval logs, each log will have `samples * epochs` transcripts. If you are reading from another source, each agent trace will yield a single `Transcript`. + +### Transcript Fields + +{{< include _transcript_fields.md >}} + +### Content Filtering + +Note that the `messages` and `events` fields will not be populated unless you specify a `messages` or `events` filter on your scanner. For example, this scanner will see all messages and events: + +``` python +@scanner(messages="all", events="all") +def my_scanner() -> Scanner[Transcript]: ... +``` + +This scanner will see only model and tool events: + +``` python +@scanner(events=["model", "tool"]) +def my_scanner() -> Scanner[Transcript]: ... +``` + +This scanner will see only assistant messages: + +``` python +@scanner(messages=["assistant"]) +def my_scanner() -> Scanner[Transcript]: ... +``` + +### Presenting Messages + +When processing transcripts, you will often want to present an entire message history to a model for analysis. The `message_numbering()` function provides numbered message formatting and reference extraction: + +``` python +# setup message numbering +messages_as_str, extract_refs = message_numbering() + +# call model +output = await get_model().generate( + "Here is a transcript of an LLM agent " + + "solving a puzzle:\n\n" + + "===================================" + + await messages_as_str(transcript.messages) + + "===================================\n\n" + + "In the transcript above do you see the agent " + + "becoming confused? Respond beginning with 'Yes' " + + "or 'No', followed by an explanation." +) + +# extract references from the model's explanation +explanation = output.completion +references = extract_refs(explanation) +``` + +The `message_numbering()` function returns a `(messages_as_str, extract_refs)` pair: + +- `messages_as_str()` converts a list of messages into a numbered string representation, using auto-incrementing labels (`[M1]`, `[M2]`, etc.). If called multiple times within the same numbering scope, numbering continues where it left off (e.g. the second call starts at `[M6]` if the first call rendered five messages). + +- `extract_refs()` resolves citations like `[M3]` in model output back to message IDs, producing `Reference` objects suitable for `Result.references`. + +You can optionally pass a `MessagesPreprocessor` to `message_numbering()` to control which messages are included. Available options include `exclude_system`, `exclude_reasoning`, and `exclude_tool_usage`. + +## Event Scanners {#events} + +To write a scanner that targets events, write a function that takes the event type(s) you want to process. For example, this scanner will see only model events: + +``` python +@scanner +def my_scanner() -> Scanner[ModelEvent]: + def scan(event: ModelEvent) -> Result: + ... + + return scan +``` + +Note that the `events="model"` filter was not required since we had already declared our scanner to take only model events. If we wanted to take both model and tool events we'd do this: + +``` python +@scanner +def my_scanner() -> Scanner[ModelEvent | ToolEvent]: + def scan(event: ModelEvent | ToolEvent) -> Result: + ... + + return scan +``` + +## Message Scanners {#messages} + +To write a scanner that targets messages, write a function that takes the message type(s) you want to process. For example, this scanner will only see tool messages: + +``` python +@scanner +def my_scanner() -> Scanner[ChatMessageTool]: + def scan(message: ChatMessageTool) -> Result: + ... + + return scan +``` + +This scanner will see only user and assistant messages: + +``` python +@scanner +def my_scanner() -> Scanner[ChatMessageUser | ChatMessageAssistant]: + def scan(message: ChatMessageUser | ChatMessageAssistant) -> Result: + ... + + return scan +``` + +## Timeline Scanners {#timelines} + +Timelines provide a hierarchical view of agent execution, organizing flat events into a tree of spans that represent agent invocations, tool calls, and other structured activities. While flat message and event lists work well for simple transcripts, timelines are essential for understanding multi-agent or deeply nested agent workflows. + +To create a timeline scanner, use the `timeline` filter on the `@scanner` decorator or annotate your scanner with the `Timeline` type. You can also filter which event types are included in the timeline: + +``` python +@scanner(timeline=True) +def my_scanner() -> Scanner[Timeline]: ... + +@scanner(timeline=["model", "tool"]) +def my_scanner() -> Scanner[Timeline]: ... +``` + +Timeline scanning differs from transcript scanning in that each span in the timeline tree is scanned independently, allowing you to analyze individual agent invocations and their relationships. + +Note that timeline scanners are available only in the development version of Inspect Scout. See the [Multi Agent](multi_agent.qmd) article for a full description of the timeline data model and examples of timeline scanners. + +## Multiple Results {#multiple-results} + +Scanners can return multiple results as a list. For example: + +``` python +return [ + Result(label="deception", value=True, explanation="..."), + Result(label="misconfiguration", value=True, explanation="...") +] +``` + +This is useful when a scanner is capable of making several types of observation. In this case it's also important to indicate the origin of the result (i.e. which class of observation is is), which you can do using the `label` field (note that `label` can repeat multiple times in a set, so e.g. you could have multiple results with `label="deception"`). + +When a list is returned, each individual result will yield its own row in the [results data frame](results.qmd#data-frames). + +When validating scanners that return lists of results, you can use [result set validation](validation.qmd#result-set-validation) to specify expected values for each label independently. + +## Custom Loaders {#loaders} + +When you want to process multiple discrete items from a `Transcript` this might not always fall neatly into single messages or events. For example, you might want to process pairs of user/assistant messages. To do this, create a custom `Loader` that yields the content as required. + +For example, here is a `Loader` that yields user/assistant message pairs: + +``` python +@loader(messages=["user", "assistant"]) +def conversation_turns(): + async def load( + transcript: Transcript + ) -> AsyncIterator[list[ChatMessage], None]: + + for user,assistant in message_pairs(transcript.messages): + yield [user, assistant] + + return load +``` + +Note that just like with scanners, the loader still needs to provide a `messages=["user", "assistant"]` in order to see those messages. + +We can now use this loader in a scanner that looks for refusals: + +``` python +@scanner(loader=conversation_turns()) +def assistant_refusals() -> Scanner[list[ChatMessage]]: + + async def scan(messages: list[ChatMessage]) -> Result: + user, assistant = messages + return Result( + value=is_refusal(assistant.text), + explanation=await messages_as_str(messages) + ) + + return scan +``` + +## Packaging {#packaging} + +A convenient way to distribute scanners is to include them in a Python package. This makes it very easy for others to use your scanner and ensure they have all of the required dependencies. + +Scanners in packages can be *registered* such that users can easily refer to them by name from the CLI. For example, if your package is named `myscanners` and your scanner is named `reward_hacking` you could do a scan with: + +```bash +scout scan myscanners/reward_hacking +``` + +### Example + +Here's an example that walks through all of the requirements for registering scanners in packages. Let's say your package is named `myscanners` and has a task named `reward_hacking` in the `scanners.py` file: + +``` +myscanners/ + myscanners/ + scanners.py + _registry.py + pyproject.toml +``` + +The `_registry.py` file serves as a place to import things that you want registered with Inspect. For example: + +``` {.python filename="_registry.py"} +from .scanners import reward_hacking +``` + +You can then register `reward_hacking` (and anything else imported into `_registry.py`) as a [setuptools entry point](https://setuptools.pypa.io/en/latest/userguide/entry_point.html). This will ensure that inspect can resolve references to your package from the CLI. Here is how this looks in `pyproject.toml`: + +::: {.panel-tabset group="entry-points"} +## Setuptools + +``` toml +[project.entry-points.inspect_ai] +myscanners = "myscanners._registry" +``` + +## uv + +``` toml +[project.entry-points.inspect_ai] +myscanners = "myscanners._registry" +``` + +## Poetry + +``` toml +[tool.poetry.plugins.inspect_ai] +myscanners = "myscanners._registry" +``` +::: + +Now, anyone that has installed your package can run use your scanner as follows: + +``` bash +scout scan myscanners/reward_hacking +``` + +## Scanners as Scorers {#scanners-as-scorers} + +You may have noticed that scanners are very similar to Inspect [Scorers](https://inspect.aisi.org.uk/scorers.html). This is by design, and it is actually possible to use scanners directly as Inspect scorers. + +For example, here is a simple scanner that checks for agent confusion: + +``` python +@scanner(messages="all") +def confusion() -> Scanner[Transcript]: + return llm_scanner( + question="In the transcript above do you see " + + "the agent becoming confused?", + answer="boolean", + ) +``` + +We can use this directly in an Inspect `Task` as follows: + +``` python +from .scanners import confusion + +@task +def mytask(): + return Task( + ..., + scorer = confusion() + ) +``` + +We can also use it with the `inspect score` command: + +``` bash +inspect score --scorer scanners.py@confusion logfile.eval +``` + +### Metrics + +The metrics used for the scorer will default to `mean()` and `stderr()`---however, you can also explicitly specify metrics on the `@scanner` decorator: + +``` python +@scanner(messages="all", metrics=[mean(), bootstrap_stderr()]) +def confusion() -> Scanner[Transcript]: ... +``` + +If you are interfacing with code that expects only `Scorer` instances, you can also use the `as_scorer()` function from Inspect Scout to explicitly convert your scanner to a scorer: + +``` python +from inspect_ai import eval +from inspect_scout import as_scorer + +from .mytasks import ctf_task +from .scanners import confusion + +eval(ctf_task(scorer=as_scorer(confusion()))) +``` + +### Result Sets + +If your scanner yields [multiple results](#multiple-results) you can still use it as a scorer, but you will want to provide a dictionary of metrics corresponding to the labels used by your results. For example, if you have a scanner that can yield results with `label="deception"` or `label="misconfiguration"`, you might declare your metrics like this: + +``` python +@scanner(messages="all", metrics=[{ "deception": [mean(), stderr()], "misconfiguration": [mean(), stderr()] }]) +def my_scanner() -> Scanner[Transcript]: ... +``` + +Or you can use a glob (\*) to use the same metrics for all labels: + +``` python +@scanner(messages="all", metrics=[{ "*": [mean(), stderr()] }]) +def my_scanner() -> Scanner[Transcript]: ... +``` + +You should also be sure to return a result for each supported label (so that metrics can be computed correctly on each row). + +If your scanner yields [multiple results](#multiple-results) see the discussion above on [Result Sets](#result-sets) for details on how to specify metrics for this case. + +## Scanner Metrics + +{{< include _scanner_metrics.md >}} diff --git a/docs/db_importing.qmd b/docs/db_importing.qmd index 7d85fa122..2a7996814 100644 --- a/docs/db_importing.qmd +++ b/docs/db_importing.qmd @@ -8,13 +8,15 @@ You can populate a transcript database in a variety of ways depending on where y 1. [Inspect Logs](#inspect-logs): Read transcript data from Inspect eval log files. -2. [Arize Phoenix](#arize-phoenix), [LangSmith](#langsmith), and [Logfire](#logfire): Read transcript data from LLM observability platforms. +2. [Arize Phoenix](#arize-phoenix), [LangSmith](#langsmith), [Logfire](#logfire), [MLFlow](#mlflow), and [W&B Weave](#wb-weave): Read transcript data from LLM observability platforms. -2. [Transcript API](#transcript-api): Python API for creating and inserting transcripts. +3. [Claude Code](#claude-code): Read transcript data from local Claude Code sessions. -3. [Arrow Import](#arrow-import): Efficient direct insertion using `RecordBatchReader`. +4. [Transcript API](#transcript-api): Python API for creating and inserting transcripts. -4. [Parquet Data Lake](#parquet-data-lake): Use an existing data lake not created using Inspect Scout. +5. [Arrow Import](#arrow-import): Efficient direct insertion using `RecordBatchReader`. + +6. [Parquet Data Lake](#parquet-data-lake): Use an existing data lake not created using Inspect Scout. We'll cover each of these in turn below. Before proceeding though you should be sure to familiarize yourself with the [Database Schema](db_schema.qmd) and make a plan for how you want to map your data into it. @@ -151,6 +153,128 @@ async with transcripts_db("s3://my-transcript-db/") as db: Set the `LOGFIRE_READ_TOKEN` environment variable to authenticate with Logfire. You can create a read token from [Logfire Settings > Read Tokens](https://logfire.pydantic.dev/). ::: +## MLFlow + +[MLFlow](https://mlflow.org/genai/observability) is an LLM and Agent Observability and Evaluations platform. Scout can import traces from MLFlow, with filtering by experiment name and/or tracking URI. + +To import transcripts from MLFlow, first install the `inspect-mlflow` package: + +```bash +pip install inspect-mlflow +``` + +Then, use the `import_mlflow_traces()` function to import traces. For example: + +```python +from inspect_mlflow.scout import import_mlflow_traces +from inspect_scout import transcripts_db + +async with transcripts_db("./my-transcripts") as db: + await db.insert(import_mlflow_traces( + experiment_name="inspect-mlflow-demo", + tracking_uri="http://localhost:5000", + )) +``` + +Be sure to also specify required credentials using environment variables or other means before conducing an import. + +## W&B Weave + +[W&B Weave](https://wandb.ai/site/weave) is Weights & Biases' tracing and evaluation framework for LLM applications. Scout can import transcripts from Weave traces, supporting: + +- OpenAI API calls +- Anthropic API calls +- Google/Gemini API calls +- Custom instrumented code (via Weave ops) + +Use the `weave()` transcript source to import traces from a Weave project. The `project` parameter should be in `"entity/project"` format: + +```python +from inspect_scout import transcripts_db +from inspect_scout.sources import weave + +async with transcripts_db("s3://my-transcript-db/") as db: + await db.insert(weave( + project="my-team/my-project", + )) +``` + +You can filter traces by time range, apply call filters, or limit the number of transcripts: + +```python +from datetime import datetime + +# Filter by time range +await db.insert(weave( + project="my-team/my-project", + from_time=datetime(2025, 1, 1), + to_time=datetime(2025, 6, 30), +)) + +# Apply call filters +await db.insert(weave( + project="my-team/my-project", + filter={"op_name": "my_agent"}, +)) + +# Limit number of transcripts +await db.insert(weave( + project="my-team/my-project", + limit=100, +)) +``` + +::: {.callout-note} +## Authentication + +Set the `WANDB_API_KEY` environment variable to authenticate with W&B. You can create an API key from [W&B Settings](https://wandb.ai/settings). +::: + +## Claude Code + +[Claude Code](https://docs.anthropic.com/en/docs/claude-code) is Anthropic's agentic coding tool. Scout can import transcripts directly from Claude Code's session files, which are normally stored at `~/.claude/projects/`. + +Use the `claude_code()` transcript source to import sessions. Each session can contain multiple conversations separated by `/clear` commands — each conversation segment becomes a separate transcript. + +```python +from inspect_scout import transcripts_db +from inspect_scout.sources import claude_code + +async with transcripts_db("s3://my-transcript-db/") as db: + await db.insert(claude_code()) +``` + +By default, all sessions across all projects are imported. You can narrow the import by specifying a project path, session ID, or time range: + +```python +# Import sessions from a specific project +await db.insert(claude_code( + path="~/dev/my-project", +)) + +# Import a specific session by ID +await db.insert(claude_code( + session_id="abc123-def456", +)) + +# Import sessions from a time range +from datetime import datetime +await db.insert(claude_code( + from_time=datetime(2025, 1, 1), + to_time=datetime(2025, 6, 30), +)) +``` + +Claude Code transcripts are mapped to the database schema as follows: + +| Field | Value | +|-------|-------| +| `source_type` | `"claude_code"` | +| `task_set` | Project directory path | +| `task_id` | Session slug (conversation summary) | +| `agent` | `"claude-code"` | + + ## Transcript API You can import transcripts from any source so long as you can create `Transcript` objects to be imported. In this example imagine we have a `read_weave_transcripts()` function which can read transcripts from an external JSON transcript format: @@ -327,3 +451,18 @@ scout db index s3://my-transcripts-data-lake/cyber ``` You should run this command whenever you add or remove transcripts from your data lake (transcripts will not be visible to clients until the index is updated). While building an index is not required, it is highly reccommend if you want optimal query performance. + + +## CLI Import + +The `scout import` command provides a CLI alternative to the Python API for importing transcripts from any registered source. For example: + +```bash +scout import claude_code +scout import phoenix -P project=my-project +scout import langsmith -P project=my-project --limit 100 +scout import weave -P project=my-team/my-project +``` + +Use `scout import --sources` to list available sources and their parameters, or `--dry-run` to preview what would be imported without writing. See [scout import](reference/scout_import.qmd) for full details. + diff --git a/docs/db_overview.qmd b/docs/db_overview.qmd index ca5fd3a95..5be87c64c 100644 --- a/docs/db_overview.qmd +++ b/docs/db_overview.qmd @@ -25,7 +25,7 @@ These articles cover transcript databases in more depth: 2. [Capturing Transcripts](db_capturing.qmd) --- Describes how to capture transcripts from running LLM code using the `@observe` decorator / context-manager. -3. [Importing Transcripts](db_importing.qmd) --- Covers building a database from Inspect Logs, Arize Phoenix, LangSmith, Logfire, and custom sources using the import API. +3. [Importing Transcripts](db_importing.qmd) --- Covers building a database from Inspect Logs, Arize Phoenix, LangSmith, Logfire, Claude Code, and custom sources using the import API. ## Publishing Transcripts diff --git a/docs/db_publishing.qmd b/docs/db_publishing.qmd index 87b2280f6..affcd4efd 100644 --- a/docs/db_publishing.qmd +++ b/docs/db_publishing.qmd @@ -4,11 +4,7 @@ title: Publishing Transcripts ## Overview -In this article we'll cover recommended ways to publish transcript databases for use by others. Whenever publishing transcripts you should be mindful to do everything you can to prevent them from entering the training data of models (as this may "leak" benchmark datasets). The main mitigations available for this are: - -1. Making access to the transcripts authenticated (e.g. S3 or Hugging Face); and - -2. Encrypting the transcript database files so that if they are republished in an unauthenticated context that crawlers won't be able to read them. +In this article we'll cover recommended ways to publish transcript databases for use by others. Whenever publishing transcripts you should be mindful to do everything you can to prevent them from entering the training data of models (as this may "leak" benchmark datasets). The main mitigation available for this is making access to the transcripts authenticated (e.g. S3 or Hugging Face). We'll cover both of these scenarios in detail below. @@ -42,8 +38,6 @@ To access a dataset on Hugging Face: scout scan scanner.py -T hf://datasets/account-name/dataset-name ``` -See [Encryption](#encryption) below for details on adding encryption to database files as an additional measure of protection from crawlers. - ## S3 Publishing transcripst databases to AWS [S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) enables you to configure authenticated access using S3 credentials. S3 buckets support a wide variety of options for authorization (see the [documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-management.html) for further details). @@ -53,45 +47,3 @@ After you have uploaded the parquet file(s) for your transcript database to an S ``` bash scout scan scanner.py -T s3://my-transcript-databases/database-name ``` - -See [Encryption](#encryption) below for details on adding encryption to database files as an additional measure of protection from crawlers. - -## Encryption {#encryption} - -You can optionally use encryption to provide further protection for transcript databases. To encrypt a database, use the `scout db encrypt` command, passing it a valid AES encryption key (16, 24, or 32 bytes). For example: - -``` bash -scout db encrypt /path/to/my/database \ - --output-dir /path/to/my/database-enc \ - --key 0123456789abcdef -``` - -If you don't want to include the key in a script, you can also pass it via stdin (`--key -`) or pass it via the `SCOUT_DB_ENCRYPTION_KEY` environment variable. - - -### Reading Encrypted Databases - -When using an encrypted database during a scan, you should set the `SCOUT_DB_ENCRYPTION_KEY` environment variable to the appropriate key. For example: - -``` bash -export SCOUT_DB_ENCRYPTION_KEY=0123456789abcdef -scout scan scanner.py -T /path/to/my/database-enc -``` - -You can also decrypt the database using the `scout db decrypt` command: - -``` bash -scout db decrypt /path/to/my/database-enc \ - --output-dir /path/to/my/database \ - --key 0123456789abcdef -``` - -### Limitations - -Scout uses DuckDB [Parquet Encryption](https://duckdb.org/docs/stable/data/parquet/encryption) to implement encryption. While this will provide additional protection for data, there are some drawbacks: - -1. It is not currently compatible with the encryption of, e.g., PyArrow, so encrypted Parquet files will currently only be readable with DuckDB. - -2. Compression ratios for encrypted Parquet are much lower than for unencrypted (e.g. database files might be 5-8 times larger). - -3. Read performance may be a bit slower due to decryption (but it's unlikely this will matter as most time in scanning is spent on inference not reading). \ No newline at end of file diff --git a/docs/db_schema.qmd b/docs/db_schema.qmd index 4250789b3..54e2a38a0 100644 --- a/docs/db_schema.qmd +++ b/docs/db_schema.qmd @@ -72,10 +72,10 @@ While you can include any of the event types in defined in [inspect_ai.event](ht Most observability systems will have some equivalent of the above in their traces. When reconstructing model events you will also likely want to use the helper functions mentioned above in [Messages](#messages) for converting raw model API payloads to `ChatMessage`. -::: callout-note -#### Not Required +::: {.callout-important title="events_data"} +If you are including `events` you should also include an `events_data` field to reduce the size of your transcripts. Note that model events include the entire `input` so for long trajectories the storage requirements are O(n^2^). -The `events` field is only important if you have scanners that will be doing event analysis. Note that the default `llm_scanner()` provided within Scout looks only at `messages` not `events`. +Use the Inspect AI [condense_events()](https://inspect.aisi.org.uk/reference/inspect_ai.log.html#condense_events) function to take `events` and split them into `events` and `events_data`. The events will be automatically reconstructed when being scanned or viewed. ::: ## Schema in Code diff --git a/docs/images/event-timeline.png b/docs/images/event-timeline.png new file mode 100644 index 000000000..61e2ffebc Binary files /dev/null and b/docs/images/event-timeline.png differ diff --git a/docs/index.qmd b/docs/index.qmd index 447e6803a..e04edaf7f 100644 --- a/docs/index.qmd +++ b/docs/index.qmd @@ -10,10 +10,11 @@ lightbox: true Welcome to Inspect Scout, a tool for in-depth analysis of AI agent transcripts. With Scout, you can easily: 1. Detect issues like misconfigured environments, refusals, and evaluation awareness using LLM-based or pattern-based scanners. -2. Analyze transcripts from Inspect Evals, Arize Phoenix, LangSmith, Logfire, or custom sources via the capture and import APIs. +2. Analyze transcripts from Inspect, Arize Phoenix, LangSmith, Logfire, MLFLow, W&B Weave, Claude Code, or custom sources via the capture and import APIs. 3. Develop scanners interactively, exploring transcripts and scan results visually in Scout View. 4. Validate scanner accuracy against human-labeled examples. -5. Scale to thousands of transcripts with parallel processing, batching, and fault tolerance. +5. Handle complex scanning requirements like multi-agent transcripts, compaction, and context-window chunking. +6. Scale to thousands of transcripts with parallel processing, batching, and fault tolerance. ### Installation @@ -38,95 +39,17 @@ Below we'll provide some simple examples of creating and using Scout scanners. S ### LLM Scanner -For many application you can use the high-level `llm_scanner()`, which uses a model for transcript analysis and can be customized with many options. For example: +{{< include _llm_scanner.md >}} -``` {.python filename="scanner.py"} -from inspect_scout import Scanner, Transcript, llm_scanner, scanner - -@scanner(messages="all") -def ctf_environment() -> Scanner[Transcript]: - return llm_scanner( - question="In the transcript above do you detect " - "instances of environment misconfiguration " - "preventing the agent from completing it's task?", - answer="boolean" - ) -``` - -The `llm_scanner()` supports a wide variety of model answer types including boolean, number, string, classification (single or multi), and structured JSON output. For additional details, see the [LLM Scanner](llm_scanner.qmd) article. ### Grep Scanner -Using an LLM to search transcripts is often required for more nuanced judgements, but if you are just looking for text patterns, you can also use the `grep_scanner()`. For example, here we search assistant messages for references to phrases that might indicate secrets: - -```python -from inspect_scout import Transcript, grep_scanner, scanner - -@scanner(messages=["assistant"]) -def secrets() -> Scanner[Transcript]: - return grep_scanner(["password", "secret", "token"]) -``` - -For additional details on using this scanner, see the [Grep Scanner](grep_scanner.qmd) article. - -### Custom Scanner - -If the higher level LLM and Grep scanners don't meet your requirements, you can write a custom scanners with whatever behavior you need. For example, the following illustrates the implementation of a basic LLM scanner (click on the numbers at right for further explanation): +{{< include _grep_scanner.md >}} -``` {.python filename="scanner.py"} -from inspect_scout import ( - Result, Scanner, Transcript, scanner, messages_as_str -) - -from inspect_ai.model import get_model -import re - -@scanner(messages="all") # <1> -def ctf_environment() -> Scanner[Transcript]: - - async def scan(transcript: Transcript) -> Result: - - # call model - output = await get_model().generate( # <2> - "Here is a transcript of an LLM agent " + - "solving a cybersecurity challenge:\n\n" + - "===================================" + # <3> - await messages_as_str(transcript) + # <3> - "===================================\n\n" + # <3> - "In the transcript above do you detect any " + - "instances of environment misconfiguration " + - "preventing the agent from completing its " + - "task? If no, simply respond with 'No'. " + - "If yes, respond with 'Yes' followed by an " + - "explanation." - ) - - # extract the first word - match = re.match(r"^\w+", output.completion.strip()) - - # return result - if match: - answer = match.group(0) - return Result( # <4> - value=answer.lower() == "yes", # <4> - answer=answer, # <4> - explanation=output.completion, # <4> - ) # <4> - else: - return Result(value=False, explanation=output.completion) - - return scan -``` - -1. Scanners are decorated with `@scanner` so they can specify the exact subset of content they need to read. In this case only messages (and not events) will be read from the log, decreasing load time. - -2. Scanners frequently use models to perform scanning. Calling `get_model()` utilizes the default model for the scan job (which can be specified in the top level call to scan). -3. Convert the message history into a string for presentation to the model. The `messages_as_str()` function takes a `Transcript | list[Messages]` and will by default remove system messages from the message list. See `MessagesPreprocessor` for other available options. +### Custom Scanners -4. As with scorers, results also include additional context (here the extracted answer and full model completion). - -Above we used only the `messages` field from the `transcript`, but `Transcript` includes many other fields with additional context. See [Transcript Fields](transcripts.qmd#transcript-fields) for additional details. +{{< include _custom_scanner.md >}} ### Running a Scan @@ -193,48 +116,7 @@ See the [Projects](projects.qmd) article for more details on managing configurat ## Scan Jobs -You may want to import scanners from other modules and compose them into a `ScanJob`. To do this, add a `@scanjob` decorated function to your source file (it will be used in preference to `@scanner` decorated functions). - -A `ScanJob` can also include `transcripts` or any other option that you can pass to `scout scan` (e.g. `model`). For example: - -``` {.python filename="scanning.py"} -from inspect_scout import ScanJob, scanjob - -@scanjob -def job() -> ScanJob: - return ScanJob( - scanners=[ctf_environment(), java_tool_usages()], - transcripts="./logs", - model="openai/gpt-5" - ) -``` - -You can then use the same command to run the job (`scout scan` will prefer a `@scanjob` defined in a file to individual scanners): - -``` bash -scout scan scanning.py -``` - -You can also specify a scan job using YAML or JSON. For example, the following is equivalent to the example above: - -``` {.yaml filename="scan.yaml"} -scanners: - - name: deception - file: scanner.py - - name: java_tool_usages - file: scanner.py - -transcripts: logs -filter: task_set='cybench' - -model: openai/gpt-5 -``` - -Which can be executed with: - -``` bash -scout scan scan.yaml -``` +{{< include _scan_jobs.md >}} Note that if you had a scout.yaml [project file](#projects) defining the `transcripts`, `filter`, and `model` for your project, you could exclude them from your scan job as they will be automatically merged from the project. @@ -338,14 +220,14 @@ See the article on [Transcripts](transcripts.qmd) to learn more about the variou Above we provided a high-level tour of Scout features. See the following articles to learn more about using Scout: +- [Scanners](scanners.qmd): Basics of using scanners including the high-level [LLM Scanner](llm_scanner.qmd) and [Grep Scanner](grep_scanner.qmd). + - [Examples](examples.qmd): Example implementations of various types of scanners. - [Projects](projects.qmd): Managing scanning configuration using project files. - [Transcripts](transcripts.qmd): Reading and filtering transcripts for scanning. -- [LLM Scanner](llm_scanner.qmd) and [Grep Scanner](grep_scanner.qmd): Higher-level scanners for model and pattern-based scanning of transcripts. - - [Workflow](workflow.qmd): Workflow for the stages of a transcript analysis project. -There is also more in depth documentation available on [Scanners](scanners.qmd), [Results](results.qmd), [Validation](validation.qmd) and [Transcript Databases](db_overview.qmd). \ No newline at end of file +There is also more in depth documentation available on [Results](results.qmd), [Validation](validation.qmd) and [Transcript Databases](db_overview.qmd). \ No newline at end of file diff --git a/docs/llm_scanner.qmd b/docs/llm_scanner.qmd index e6dec8b12..4dac2a5c6 100644 --- a/docs/llm_scanner.qmd +++ b/docs/llm_scanner.qmd @@ -59,6 +59,24 @@ The `answer` type determines how the LLM is prompted to respond, the way that an | labels (multiple) | ANSWER: C, D | `list[str]` | | structured | JSON object | `dict[str,JsonValue]` | +Note that passing `list[str]` prompts the model to select a **single** label. To allow **multiple** label selections, wrap the labels in `AnswerMultiLabel`: + +``` python +from inspect_scout import AnswerMultiLabel + +@scanner(messages="all") +def issue_categories() -> Scanner[Transcript]: + return llm_scanner( + question="What issues are present in this conversation?", + answer=AnswerMultiLabel(labels=[ + "Factual error", + "Refusal", + "Off-topic response", + "Hallucination", + ]) + ) +``` + For details on JSON object answers, see the [Structured Answers](#structured-answers) section below. ## Prompt Template @@ -358,4 +376,121 @@ Dynamic questions are useful when: - The question depends on transcript metadata. - You need to reference specific aspects of the conversation in your question -- The same scanner needs to adapt its question based on context \ No newline at end of file +- The same scanner needs to adapt its question based on context + +## Segmentation + +### Context Window + +When a transcript's message history exceeds the scanning model's context window, `llm_scanner()` automatically segments the messages to fit within 80% of the model's available context. Each segment is scanned independently with the same prompt, and results from multiple segments are combined using a reducer. + +The default reducer is selected based on the answer type: + +| Answer Type | Default Reducer | +|-------------|----------------| +| `"boolean"` | `ResultReducer.any` | +| `"numeric"` | `ResultReducer.mean` | +| `"string"` | `ResultReducer.llm()` | +| labels | `ResultReducer.majority` | +| `AnswerMultiLabel` | `ResultReducer.union` | +| `AnswerStructured` | `ResultReducer.last` | +| `list[AnswerStructured]` | `ResultReducer.union` | + +You can override the reducer with a custom function or use `ResultReducer.llm()` for LLM-based synthesis of multi-segment results. + +Note that if you have a structured scanner that returns a `list[BaseModel]` then those results will be auto‑combined via union so there is no need to specify a reducer. + +Use the `context_window` option of `llm_scanner()` to set a custom threshold (again, the default is 80% of available context). + +### Compaction + +During long-running agent tasks, the agent framework may _compact_ the conversation history—summarizing, trimming, or editing it—to stay within the model's context window. These compaction events create natural boundaries in the message history. + +The `compaction` parameter controls how `llm_scanner()` handles these boundaries when extracting messages to scan: + +``` python +# Scan all compaction regions (default) +scanner = llm_scanner( + question="Analyze the full conversation for reward hacking issues.", + answer="boolean", + compaction="all", +) + +# Scan only the final generation's messages +scanner = llm_scanner( + question="Did the model produce a correct answer?", + answer="boolean", + compaction="last", +) + +# Keep the last 3 compaction regions +scanner = llm_scanner( + question="Summarize the key decisions made during the conversation.", + answer="string", + compaction=3, +) +``` + +| Value | Behavior | +|-------|----------| +| `"all"` (default) | Merges across all compaction boundaries, reconstructing the full conversation for comprehensive analysis. | +| `"last"` | Scans only the messages that led to the model's final generation. Use this when earlier context is irrelevant or when you want to focus on the final answer. | +| integer `n` | Keeps the last *n* compaction regions, merged together. Provides a middle ground between full coverage and recency focus. | + +Compaction determines _which_ messages to extract; [context window](#context-window) segmentation then splits those messages to fit the scanner model's context. The two work together sequentially: compaction selects the region of interest, then context window segmentation ensures each piece fits within the scanning model's limits. + + +## Scanning Timelines + +A timeline is a tree of **spans** representing the structure of an agent's execution. Each span corresponds to an agent, tool, or scorer invocation, and contains **events** (model calls, tool calls, compaction) with optional child spans. Timelines are built automatically from transcript events—they detect agent hierarchies, conversation threads, re-rolled attempts (branches), and utility agents. + +Utility spans (single-turn helper agents with different system prompts) are excluded from scanning by default, so the scanner focuses on the substantive agent interactions. + +### Opting In + +To scan timelines instead of raw messages, decorate your scanner with `@scanner(timeline=True)`: + +``` python +from inspect_scout import llm_scanner, scanner + +@scanner(timeline=True) +def my_scanner(): + return llm_scanner( + question="...", + answer="boolean", + ) +``` + +When timeline scanning is enabled, each span in the timeline tree is scanned independently—the LLM sees only that span's conversation. This is particularly powerful with structured list answers, where each span produces its own list of findings that naturally roll up into a combined result set across all spans. + +### Structured Lists + +Structured list answers (`list[MyModel]`) are ideal for timeline scanning because they preserve per-span detail without needing a reducer to collapse results: + +``` python +from pydantic import BaseModel, Field +from inspect_scout import ( + AnswerStructured, llm_scanner, scanner, +) + +class Finding(BaseModel): + category: str = Field(alias="label", description="Category of the finding.") + detail: str = Field(alias="explanation", description="Explain the finding, citing message numbers.") + +@scanner(timeline=True) +def timeline_findings(): + return llm_scanner( + question="Report any notable behaviors in the conversation.", + answer=AnswerStructured(type=list[Finding]) + ) +``` + +For simple answer types (boolean, numeric), the result from each span is reduced using the default reducer for that type. + +### Depth Control + +Use the `depth` parameter to limit how deep into the span tree to scan: + +- `depth=1` — scan only the root span +- `depth=2` — scan the root and its immediate children +- `depth=None` (default) — scan all levels diff --git a/docs/llms.txt b/docs/llms.txt index a093e5ce0..6b63fe11b 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -6,16 +6,22 @@ - [Welcome](https://meridianlabs-ai.github.io/inspect_scout/index.html.md): Installation and tutorial on basic usage. - [Examples](https://meridianlabs-ai.github.io/inspect_scout/examples.html.md): Example implementations for various types of scanners. +- [Scanners](https://meridianlabs-ai.github.io/inspect_scout/scanners.html.md): Basics of using scanners. +- [Transcripts](https://meridianlabs-ai.github.io/inspect_scout/transcripts.html.md): Reading and filtering transcripts for scanning. - [Workflow](https://meridianlabs-ai.github.io/inspect_scout/workflow.html.md): End to end workflow for transcript analysis projects. - [Projects](https://meridianlabs-ai.github.io/inspect_scout/projects.html.md): Using projects to manage scanning configuration. -- [Transcripts](https://meridianlabs-ai.github.io/inspect_scout/transcripts.html.md): Reading and filtering transcripts for scanning. -- [Scanners](https://meridianlabs-ai.github.io/inspect_scout/scanners.html.md): Implementing custom scanners and loaders. -- [LLM Scanner](https://meridianlabs-ai.github.io/inspect_scout/llm_scanner.html.md): Customizable LLM scanner for model evaluation of transcripts. -- [Grep Scanner](https://meridianlabs-ai.github.io/inspect_scout/grep_scanner.html.md): Customizable scanner for text-pattern searching of transcripts. - [Results](https://meridianlabs-ai.github.io/inspect_scout/results.html.md): Collecting and analyzing scanner results. - [Validation](https://meridianlabs-ai.github.io/inspect_scout/validation.html.md): Validation of scanner results against ground truth target values. -## Transcripts Database +## Scanners + +- [LLM Scanner](https://meridianlabs-ai.github.io/inspect_scout/llm_scanner.html.md): Customizable LLM scanner for model evaluation of transcripts. +- [Grep Scanner](https://meridianlabs-ai.github.io/inspect_scout/grep_scanner.html.md): Customizable scanner for text-pattern searching of transcripts. +- [Custom Scanners](https://meridianlabs-ai.github.io/inspect_scout/custom_scanner.html.md): Comprehensive documentation on creating custom scanners. +- [Scanner Tools](https://meridianlabs-ai.github.io/inspect_scout/scanner_tools.html.md): Create custom scanners with the same tools used under the hood by `llm_scanner()`. +- [Timelines](https://meridianlabs-ai.github.io/inspect_scout/timelines.html.md): Scan multi-agent transcripts with compaction using timelines. + +## Transcripts - [Overview](https://meridianlabs-ai.github.io/inspect_scout/db_overview.html.md): Overview of transcript database concepts and basic usage. - [Schema](https://meridianlabs-ai.github.io/inspect_scout/db_schema.html.md): Details on the required and optional fields for databases and underlying data formats for messages and events. diff --git a/docs/multi_agent.qmd b/docs/multi_agent.qmd new file mode 100644 index 000000000..7c63bb70d --- /dev/null +++ b/docs/multi_agent.qmd @@ -0,0 +1,239 @@ +--- +title: "Multi Agent" +lightbox: true +--- + +## Overview + +Traditionally, evaluation and scanning workflows have assumed that a single conversation trajectory (message list) represents an agent's activity. However, modern coding agents like Claude Code and Codex CLI create many additional dimensions to consider. Consider this transcript from a Claude Code agent session: + +![](images/event-timeline.png){.border} + +As you can see, multiple sub-agents are created each of which feeds context back into the main agent trajectory. In many cases multiple agents are run in parallel, and compaction will often further divide trajectories into multiple-phases. Further, the total tokens consumed by these agents can often run into the tens of millions, requiring that scanning be chunked into multiple steps. + +### Example: Claude Code + +Below we describe the tools available in Scout to handle multi-agent transcripts. Before reading on, you might find it useful to explore some real multi-agent transcripts to gain intuition about their shape and behavior. + +You can import and view your own local Claude Code session transcripts as follows: + +```bash +mkdir cc-sessions +cd cc-sessions +scout import claude_code --limit 50 +scout view +``` + +As you'll see when browsing the transcripts, most Claude Code sessions make use of several sub-agents (often in parallel). You might also see one or more compactions. Below, we'll describe how you can create scanners that target transcripts like these. + +## Timelines + +A timeline is a hierarchical tree of spans built automatically from transcript events. Each span represents an agent, tool, or scorer invocation, containing events (model calls, tool calls, compaction) with optional child spans. + +When you enable `timeline=True` on a scanner, the timeline is built from the transcript's events and made available on `Transcript.timelines`. + +## Scanning Timelines + +The primary use case for timelines is scanning---analyzing each agent's conversation independently rather than treating the entire transcript as a flat message list. + +### Opting In + +To scan timelines instead of raw messages, set `timeline=True` on the `@scanner` decorator: + +``` python +from inspect_scout import llm_scanner, scanner + +@scanner(timeline=True) +def my_scanner(): + return llm_scanner( + question="Did the agent exhibit any harmful behavior?", + answer="boolean", + ) +``` + +When timeline scanning is enabled, each span in the timeline tree is scanned independently. In terms of the example above, there would be 5 distinct spans scanned: the "main" trajectory as well as the two "explore" and "code-reviewer" sub-agents. Each distinct span (main thread or sub-agent) then yields its own scanning result. + +### Depth Control {#depth} + +The `depth` parameter controls how deep into the span tree to scan: + +| `depth` | Behavior | +|----------------------------------|--------------------------------------| +| `1` | Scan only the root span — the top-level agent's full conversation thread. Treats the timeline like a flat transcript. | +| `2` | Scan the root span and its immediate children. Covers the main agent plus its direct sub-agents. | +| `None` (default) | Scan all levels recursively. Every non-utility span with model events is scanned independently. | + +: {tbl-colwidths=\[25,75\]} + +With `depth=1` you get a single scan of the entire main conversation thread. With deeper values, each sub-agent's conversation is scanned separately and results are combined. For example, `depth=2` on an agent that delegates to three sub-agents produces up to four independent scans (the root plus each child). + +``` python +@scanner(timeline=True) +def shallow_scan(): + return llm_scanner( + question="Was the overall task completed successfully?", + answer="boolean", + depth=1, # scan only the root agent's conversation + ) +``` + +### Compaction {#compaction} + +Long-running agents may have their context window compressed mid-conversation (via summarization or trimming), creating compaction boundaries that divide the conversation into regions. The `compaction` parameter controls how these boundaries are handled: + +| `compaction` | Behavior | +|-----------------------------|-------------------------------------------| +| `"all"` (default) | Merge messages across compaction boundaries, reconstructing the full conversation. The scanner sees everything. | +| `"last"` | Scan only the final region after the last compaction. Useful when you only care about recent behavior or when earlier context is too noisy. | +| `N` (int) | Keep the last *N* compaction regions. `1` is equivalent to `"last"`. | + +: {tbl-colwidths=\[25,75\]} + +``` python +@scanner(timeline=True) +def recent_behavior(): + return llm_scanner( + question="Did the agent make any errors in its recent actions?", + answer="boolean", + compaction="last", # scan only the most recent region + ) +``` + +### Context Chunking {#context-chunking} + +When a span's messages (after compaction processing) exceed the scanning model's context window, they are automatically split into segments sized to fit within 80% of the model's available context. Each segment is scanned independently with the same prompt. Use the `context_window` parameter to override the model's detected context window size. + +Because context-window segmentation can produce multiple results from a single span, those per-segment results are combined using a **reducer**. The default reducer is selected based on the answer type: + +| Answer Type | Default Reducer | +|--------------------|-----------------------| +| `"boolean"` | `ResultReducer.any` | +| `"numeric"` | `ResultReducer.mean` | +| `"string"` | `ResultReducer.llm()` | +| labels | `ResultReducer.majority` | +| `AnswerMultiLabel` | `ResultReducer.union` | +| `AnswerStructured` | `ResultReducer.last` | + +Override with the `reducer` parameter: + +``` python +from inspect_scout import ResultReducer, llm_scanner, scanner + +@scanner(timeline=True) +def synthesized_analysis(): + return llm_scanner( + question="Summarize the agent's key decisions.", + answer="string", + reducer=ResultReducer.llm(model="openai/gpt-5"), + ) +``` + +Built-in reducers include `ResultReducer.any`, `.mean`, `.median`, `.mode`, `.max`, `.min`, `.union`, `.majority`, `.last`. + +Note that reduction applies only within a single span when context-window segmentation is needed. When scanning multiple spans (depth \> 1), each span produces its own result independently, and these are collected into a `ResultSet` — no reduction occurs across spans. + +### Utility Spans + +Single-turn helper agents with different system prompts (e.g., a bash command checker or title-generation helper) are automatically classified as utility spans and excluded from scanning. This ensures scanning focuses on substantive agent interactions rather than mechanical helpers. + +## Custom Scanners + +The examples above all use the high-level `llm_scanner()`, customizing its behavior with the `depth`, `compaction`, and `reducer` options. + +You might however want to build a fully custom scanner for multi-agent transcripts---you can do this by using the lower level [Scanner Tools](scanner_tools.qmd) employed by `llm_scanner()`. These tools include: + +1. [Message Extraction](scanner_tools.qmd#message-extraction)---Extracting message lists for scanning from timelines. + +2. [Message Presentation](scanner_tools.qmd#message-presentation)---Number messages for presentation to the model and then automatically create message citations from model output. + +3. [Prompt Construction](scanner_tools.qmd#prompt-construction)---Create prompts that include numbered messages and formatting various types of answers (boolean, multi-lable, structured, etc.) + +4. [Answer Generation and Parsing](scanner_tools.qmd#answer-generation)---Executing LLM generations and parsing the answer into a `Result` including citations for messages. + +See the article on [Scanner Tools](scanner_tools.qmd) for details on using these tools in custom scanners. + +## Timeline Data Model {#data-model} + +Typically you can rely on automatic timeline construction from transcripts as most agent frameworks and coding agents annotate their event streams with agent spans that enable automatic detection. However, you may also wish to manually construct timelines from custom data sources---this section provides details on how to do this. + +### Building a Timeline + +Call `timeline_build(events)` to convert a flat event list into a hierarchical timeline. In a scanner context, timelines are built automatically when `timeline=True` — you rarely need to call it directly. The builder detects top-level phases (init → solvers → scorers), agent spans (both explicit and tool-spawned) and utility agents (single-turn helpers with different system prompts). + +### Timeline Data Types + +The timeline is composed of four types that form a recursive tree: + +#### `Timeline` + +The root container for a timeline view. + +| Field | Type | Description | +|---------------|----------------|----------------------------------| +| `name` | `str` | Name of the timeline view | +| `description` | `str` | Description of the timeline | +| `root` | `TimelineSpan` | Root span containing all content | + +#### `TimelineSpan` + +An execution span representing an agent, tool, scorer, or other invocation. + +| Field | Type | Description | +|-------------------|------------------|-----------------------------------| +| `id` | `str` | Unique identifier | +| `name` | `str` | Display name of the span | +| `span_type` | `str | None` | `"agent"`, `"tool"`, `"scorers"`, or `None` | +| `content` | `list[TimelineEvent | TimelineSpan]` | Child events and spans | +| `branches` | `list[TimelineBranch]` | Discarded alternative paths | +| `utility` | `bool` | Whether this is a utility span (excluded from scanning) | +| `description` | `str | None` | Optional description | +| `agent_result` | `str | None` | The agent's result, if this span represents an agent | +| `outline` | `Outline | None` | Hierarchical outline of events within the span | + +Properties: `start_time`, `end_time`, `total_tokens`. + +#### `TimelineEvent` + +Wraps a single `Event` from the transcript. + +| Field | Type | Description | +|---------|---------|-----------------------------------------------------| +| `event` | `Event` | The wrapped event (e.g., `ModelEvent`, `ToolEvent`) | + +Properties: `start_time`, `end_time`, `total_tokens`. + +#### `TimelineBranch` + +A discarded alternative path from a branch point (re-rolled attempt). + +| Field | Type | Description | +|-------------------|------------------|-----------------------------------| +| `forked_at` | `str` | ID of the event where the branch diverged | +| `content` | `list[TimelineEvent | TimelineSpan]` | Events and spans in the discarded path | + +Properties: `start_time`, `end_time`, `total_tokens`. + +#### Nesting Structure + +The tree nests as: `Timeline` → root `TimelineSpan` → `content` (a mix of `TimelineEvent` and child `TimelineSpan` nodes). Each span can also have `branches` containing `TimelineBranch` objects that capture discarded retries. + +## Filtering Timelines {#filtering} + +Use `timeline_filter()` to create a new timeline containing only spans that match a predicate: + +``` python +from inspect_ai.event import timeline_build, timeline_filter + +timeline = timeline_build(events) + +# Keep only agent spans +agents_only = timeline_filter(timeline, lambda s: s.span_type == "agent") + +# Exclude scorer spans +no_scorers = timeline_filter(timeline, lambda s: s.span_type != "scorers") + +# Exclude utility agents (single-turn helpers) +no_utility = timeline_filter(timeline, lambda s: not s.utility) +``` + +The filter recursively walks the span tree, pruning non-matching spans and their subtrees. `TimelineEvent` items are always kept (they belong to the parent span). This is useful for pre-filtering a timeline before programmatic analysis. \ No newline at end of file diff --git a/docs/reference/_reference.md b/docs/reference/_reference.md index 84d8c2fd7..1b7ff43a5 100644 --- a/docs/reference/_reference.md +++ b/docs/reference/_reference.md @@ -10,7 +10,7 @@ | [Sources](/reference/sources.qmd) | Import transcripts from various sources. | | [Async](/reference/async.qmd) | Async functions for scanning. | -#### Scount CLI +#### Scout CLI | | | |------------------------------------|------------------------------------| diff --git a/docs/reference/_sidebar.yml b/docs/reference/_sidebar.yml index 0bed05c95..29387f306 100644 --- a/docs/reference/_sidebar.yml +++ b/docs/reference/_sidebar.yml @@ -31,8 +31,8 @@ website: href: reference/scanning.qmd#scannerwork - text: Worklist href: reference/scanning.qmd#worklist - - text: Status - href: reference/scanning.qmd#status + - text: ScanSpec + href: reference/scanning.qmd#scanspec - text: ScanOptions href: reference/scanning.qmd#scanoptions - text: ScanRevision @@ -79,6 +79,8 @@ website: href: reference/transcript.qmd#transcripts_from - text: Transcript href: reference/transcript.qmd#transcript + - text: TranscriptInfo + href: reference/transcript.qmd#transcriptinfo - text: Transcripts href: reference/transcript.qmd#transcripts - text: TranscriptsReader @@ -103,6 +105,18 @@ website: href: reference/transcript.qmd#logcolumns - text: log_columns href: reference/transcript.qmd#log_columns + - text: transcript_messages + href: reference/transcript.qmd#transcript_messages + - text: timeline_messages + href: reference/transcript.qmd#timeline_messages + - text: segment_messages + href: reference/transcript.qmd#segment_messages + - text: span_messages + href: reference/transcript.qmd#span_messages + - text: MessagesSegment + href: reference/transcript.qmd#messagessegment + - text: TimelineMessages + href: reference/transcript.qmd#timelinemessages - text: observe href: reference/transcript.qmd#observe - text: observe_update @@ -130,6 +144,22 @@ website: href: reference/scanner.qmd#llm_scanner - text: grep_scanner href: reference/scanner.qmd#grep_scanner + - text: message_numbering + href: reference/scanner.qmd#message_numbering + - text: scanner_prompt + href: reference/scanner.qmd#scanner_prompt + - text: generate_answer + href: reference/scanner.qmd#generate_answer + - text: parse_answer + href: reference/scanner.qmd#parse_answer + - text: ResultReducer + href: reference/scanner.qmd#resultreducer + - text: Answer + href: reference/scanner.qmd#answer + - text: answer_type + href: reference/scanner.qmd#answer_type + - text: AnswerSpec + href: reference/scanner.qmd#answerspec - text: AnswerMultiLabel href: reference/scanner.qmd#answermultilabel - text: AnswerStructured @@ -146,6 +176,8 @@ website: href: reference/scanner.qmd#messagetype - text: EventType href: reference/scanner.qmd#eventtype + - text: TranscriptContent + href: reference/scanner.qmd#transcriptcontent - text: RefusalError href: reference/scanner.qmd#refusalerror - text: scanner @@ -163,6 +195,10 @@ website: href: reference/sources.qmd#langsmith - text: logfire href: reference/sources.qmd#logfire + - text: weave + href: reference/sources.qmd#weave + - text: claude_code + href: reference/sources.qmd#claude_code - section: async href: reference/async.qmd contents: @@ -190,4 +226,6 @@ website: - text: scout trace href: reference/scout_trace.qmd - text: scout db - href: reference/scout_db.qmd \ No newline at end of file + href: reference/scout_db.qmd + - text: scout import + href: reference/scout_import.qmd \ No newline at end of file diff --git a/docs/reference/filter/commands.py b/docs/reference/filter/commands.py index c2d496790..21a03fb42 100644 --- a/docs/reference/filter/commands.py +++ b/docs/reference/filter/commands.py @@ -7,6 +7,7 @@ import importlib import inspect +import keyword from contextlib import ExitStack, contextmanager from typing import Any, Iterator, cast @@ -26,6 +27,9 @@ def make_command_docs( """Create the Markdown lines for a command and its sub-commands.""" command = command.replace("-", "_") module = command + # Python keywords can't be module names, so we use the {command}_command convention + if keyword.iskeyword(module): + module = f"{module}_command" for line in _recursively_make_command_docs( f"scout {command}", load_command(f"inspect_scout._cli.{module}", f"{command}_command"), diff --git a/docs/reference/filter/sidebar.py b/docs/reference/filter/sidebar.py index 99d1d2c44..49d209a79 100644 --- a/docs/reference/filter/sidebar.py +++ b/docs/reference/filter/sidebar.py @@ -40,7 +40,9 @@ - text: scout trace href: reference/scout_trace.qmd - text: scout db - href: reference/scout_db.qmd + href: reference/scout_db.qmd + - text: scout import + href: reference/scout_import.qmd """) contents_yaml = sidebar["website"]["sidebar"][0]["contents"][1]["contents"] diff --git a/docs/reference/scanner.qmd b/docs/reference/scanner.qmd index f86373285..35043870a 100644 --- a/docs/reference/scanner.qmd +++ b/docs/reference/scanner.qmd @@ -16,6 +16,17 @@ reference: "inspect_scout" ### llm_scanner ### grep_scanner + +## Scanner Tools + +### message_numbering +### scanner_prompt +### generate_answer +### parse_answer +### ResultReducer +### Answer +### answer_type +### AnswerSpec ### AnswerMultiLabel ### AnswerStructured @@ -30,6 +41,7 @@ reference: "inspect_scout" ### MessageType ### EventType +### TranscriptContent ### RefusalError ## Registration diff --git a/docs/reference/scanning.qmd b/docs/reference/scanning.qmd index 325793a3a..155f20089 100644 --- a/docs/reference/scanning.qmd +++ b/docs/reference/scanning.qmd @@ -19,9 +19,9 @@ reference: inspect_scout ### ScannerWork ### Worklist -## Status +## Configuration -### Status +### ScanSpec ### ScanOptions ### ScanRevision ### ScanTranscripts diff --git a/docs/reference/scout_import.qmd b/docs/reference/scout_import.qmd new file mode 100644 index 000000000..c993ce5b5 --- /dev/null +++ b/docs/reference/scout_import.qmd @@ -0,0 +1,3 @@ +--- +title: scout import +--- diff --git a/docs/reference/sources.qmd b/docs/reference/sources.qmd index b293c452b..fc1650696 100644 --- a/docs/reference/sources.qmd +++ b/docs/reference/sources.qmd @@ -6,6 +6,8 @@ reference: "inspect_scout.sources" ### phoenix ### langsmith ### logfire +### weave +### claude_code diff --git a/docs/reference/transcript.qmd b/docs/reference/transcript.qmd index f31bfab79..fc5fff977 100644 --- a/docs/reference/transcript.qmd +++ b/docs/reference/transcript.qmd @@ -7,6 +7,7 @@ reference: "inspect_scout" ### transcripts_from ### Transcript +### TranscriptInfo ### Transcripts ### TranscriptsReader ### SampleMetadata @@ -26,6 +27,15 @@ reference: "inspect_scout" ### LogColumns ### log_columns +## Messages + +### transcript_messages +### timeline_messages +### segment_messages +### span_messages +### MessagesSegment +### TimelineMessages + ## Observe ### observe diff --git a/docs/scanner_tools.qmd b/docs/scanner_tools.qmd new file mode 100644 index 000000000..545530f9d --- /dev/null +++ b/docs/scanner_tools.qmd @@ -0,0 +1,442 @@ +--- +title: "Scanner Tools" +code-annotations: select +--- + +## Overview + +The [LLM Scanner](llm_scanner.qmd) provides a high-level, batteries-included interface for building LLM-based scanners. Under the hood, it is composed of several lower-level tools: message numbering, prompt construction, answer generation, message extraction, and result reduction. You can use these tools directly when you need more control. + +For straightforward scanning tasks, prefer `llm_scanner()` as it handles the entire pipeline for you. Use lower level scanner tools when you need to override or extend specific steps. + +Here is roughly the implementation of `llm_scanner()`, but using the lower level tools that we'll cover in more depth below (click on the numbers at right for further explanation): + +```python +from inspect_scout import ( + Result, ResultReducer, Scanner, Transcript, + generate_answer, message_numbering, scanner, + scanner_prompt, transcript_messages +) + +@scanner(messages="all") +def environment_scanner() -> Scanner[Transcript]: + + async def scan(transcript: Transcript) -> Result: + + # setup numbering and references + messages_as_str, extract_refs = message_numbering() # <1> + + # define question to ask llm + question = ( + "Did the agent fail to complete the task due to " + "a broken or misconfigured environment?" + ) + + # scan segments (breaks up transcript to fit in context window) + results: list[Result] = [] + async for segment in transcript_messages( # <2> + transcript, # <2> + messages_as_str=messages_as_str # <2> + ): # <2> + # generate prompt using segment, question, and answer type + prompt = scanner_prompt( # <3> + messages=segment.messages_str, # <3> + question=question, # <3> + answer="boolean" # <3> + ) # <3> + + # generate, extract answer/explanation and append results + results.append( + await generate_answer( # <4> + prompt, # <4> + "boolean", # <4> + extract_refs=extract_refs, # <4> + ) # <4> + ) + + # reduce per-segment results into a single result + return await ResultReducer.any(results) # <5> + + return scan +``` + +1. Setup message numbering (returns a function that can be used to number and a function that can be used to extract references to messages from model explanations). + +2. Iterate over transcript segments (if the transcript exceeds the size of the scanning model's context window it will need to be broken into segments). + +3. Build a prompt for the scanner using the string for this segment (numbered messages), the question, and the answer type. + +4. Call the model to generate an answer, extracting references from its explanation using the `extract_refs` function. + +5. When context-window segmentation produces multiple segments, reduce them into a single result. Here we use the `.any()` reducer which returns `True` if any segment result was `True`. + +## Message Presentation + +The `message_numbering()` function creates a pair of functions that share a closure-captured counter and message ID map. This enables consistent message numbering across multiple calls (e.g., across segments) and lets you extract `[M1]`-style references from model output back to the original messages. + +``` python +from inspect_scout import message_numbering + +messages_as_str, extract_refs = message_numbering() +``` + +The returned functions work together: + +- `messages_as_str(messages)` --- Renders a list of `ChatMessage` objects into a numbered string (e.g., `[M1] USER: ...`). The counter auto-increments across calls, so a second call continues from where the first left off. +- `extract_refs(text)` --- Resolves `[M1]`, `[M2]`, etc. citations in model output back to `Reference` objects pointing to the original messages. + +### Preprocessing + +You can pass a `MessagesPreprocessor` to control which message content is included in the rendered output: + +``` python +from inspect_scout import message_numbering, MessagesPreprocessor + +messages_as_str, extract_refs = message_numbering( + preprocessor=MessagesPreprocessor( + exclude_system=True, # exclude system messages (default) + exclude_reasoning=True, # exclude reasoning content + exclude_tool_usage=True, # exclude tool calls and output + ) +) +``` + +| | | +|------------------------------------|------------------------------------| +| `exclude_system` | Exclude system messages (defaults to `True`) | +| `exclude_reasoning` | Exclude reasoning content (defaults to `False`) | +| `exclude_tool_usage` | Exclude tool calls and output (defaults to `False`) | +| `transform` | Optional async function that takes the message list and returns a filtered list. | + +: {tbl-colwidths=\[35,65\]} + +When no preprocessor is provided, the default behaviour is to exclude system messages only. + +## Prompt Construction + +The `scanner_prompt()` function renders the default scanner template---the same template used internally by `llm_scanner()`. It combines the transcript messages, question, and answer type into a complete prompt: + +``` python +from inspect_scout import scanner_prompt + +prompt = scanner_prompt( + messages=segment.messages_str, + question="Did the assistant refuse the user's request?", + answer="boolean", +) +``` + +### Custom Prompts + +For fully custom prompt templates, use `answer_type()` to resolve an `AnswerSpec` into an `Answer` object that exposes `.prompt` and `.format` strings: + +``` python +from inspect_scout import answer_type + +answer = answer_type("boolean") +# answer.prompt -> "Answer the following yes or no question..." +# answer.format -> "The last line of your response should be..." + +prompt = f"""Here is a conversation: + +{segment.messages_str} + +{answer.prompt} + +{question} + +{answer.format} +""" +``` + +This gives you complete control over prompt structure while reusing the answer-type-specific formatting instructions. + +## Answer Generation {#answer-generation} + +The `generate_answer()` function calls the LLM and parses the response into a `Result`. It supports all answer types, handles refusal retries, and uses tool-based generation for structured answers: + +``` python +from inspect_scout import generate_answer + +result = await generate_answer( + prompt, + answer="boolean", + extract_refs=extract_refs, +) +``` + +| | | +|------------------------------------|------------------------------------| +| `prompt` | The scanning prompt (string or message list). | +| `answer` | Answer specification (`"boolean"`, `"numeric"`, `"string"`, `list[str]`, `AnswerMultiLabel`, or `AnswerStructured`). | +| `model` | Model to use for generation. Defaults to the current default model. | +| `retry_refusals` | Number of times to retry on content filter refusals (default `3`). | +| `parse` | When `True` (default), parse the output into a `Result`. When `False`, return the raw `ModelOutput`. | +| `extract_refs` | Function to extract `[M1]`-style references from the explanation. Only used when `parse=True`. | +| `value_to_float` | Optional function to convert the parsed value to a float. Only used when `parse=True`. | + +: {tbl-colwidths=\[25,75\]} + + +## Answer Parsing + +The `parse_answer()` function provides pure parsing without making an LLM call. Use this when you generate model output through your own code (e.g., via `get_model().generate()`) but want to use the standard answer extraction logic: + +``` python +from inspect_ai.model import get_model +from inspect_scout import parse_answer + +# generate with your own code +model = get_model("openai/gpt-4o") +output = await model.generate(prompt) + +# parse using standard answer extraction +result = parse_answer( + output, + answer="boolean", + extract_refs=extract_refs, +) +``` + +| | | +|------------------------------------|------------------------------------| +| `output` | The `ModelOutput` to parse. | +| `answer` | Answer specification (same types as `generate_answer()`). | +| `extract_refs` | Function to extract `[M1]`-style references from the explanation text. | +| `value_to_float` | Optional function to convert the parsed value to a float. | + +: {tbl-colwidths=\[25,75\]} + +## Answer Types {#answer-types} + +The `answer` parameter accepted by `scanner_prompt()`, `generate_answer()`, and `parse_answer()` is an `AnswerSpec`---a union type that determines how the LLM is prompted, how answers are extracted, and the Python type of the result value: + +| Type | LLM Output | Result Type | +|-------------------|-------------------|-----------------------| +| boolean | ANSWER: yes | `bool` | +| numeric | ANSWER: 10 | `float` | +| string | ANSWER: brown fox | `str` | +| label | ANSWER: C | `str` | +| labels (multiple) | ANSWER: C, D | `list[str]` | +| structured | JSON object | `dict[str,JsonValue]` | + +### Multi-Label Classification + +Pass `list[str]` to prompt the model to select a **single** label. To allow **multiple** label selections, wrap the labels in `AnswerMultiLabel`: + +``` python +from inspect_scout import AnswerMultiLabel + +answer = AnswerMultiLabel(labels=[ + "Factual error", + "Refusal", + "Off-topic response", + "Hallucination", +]) +``` + +Label values (`A`, `B`, `C`, ...) are assigned automatically based on position in the list. + +### Structured Answers + +`AnswerStructured` uses tool-based generation to produce JSON output conforming to a Pydantic model: + +``` python +from pydantic import BaseModel, Field +from inspect_scout import AnswerStructured + +class CyberLint(BaseModel): + misconfiguration: bool = Field( + description="Was the environment misconfigured?" + ) + tool_errors: int = Field( + description="How many tool errors were encountered?" + ) + +answer = AnswerStructured(type=CyberLint) +``` + +| | | +|------------------------------------|------------------------------------| +| `type` | Pydantic `BaseModel` subclass, or `list[Model]` for multiple results. | +| `answer_tool` | Name of the tool provided to the model (default `"answer"`). | +| `answer_prompt` | Template for the prompt that precedes the question. | +| `answer_format` | Template for instructions on how to respond. | +| `max_attempts` | Maximum retries for correct schema generation (default `3`). | + +: {tbl-colwidths=\[25,75\]} + +See [Structured Answers](llm_scanner.qmd#structured-answers) in the LLM Scanner documentation for details on Pydantic field aliases (`value`, `label`, `explanation`), multiple results via `list[Model]`, and `value_to_float` usage. + +## Message Extraction {#message-extraction} + +The `transcript_messages()` function is the primary API for extracting pre-rendered message segments from a transcript. It automatically selects the best extraction strategy based on what data is available: + +- If timelines are present, it walks the span tree and yields per-span segments. +- If only events are present, it extracts messages from events with compaction handling. +- If only messages are present, it segments them by context window. + +``` python +from inspect_scout import message_numbering, transcript_messages + +messages_as_str, extract_refs = message_numbering() + +async for segment in transcript_messages( + transcript, + messages_as_str=messages_as_str, +): + # segment.messages -> list[ChatMessage] + # segment.messages_str -> pre-rendered numbered string + # segment.segment -> 0-based segment index + ... +``` + +| | | +|------------------------------------|------------------------------------| +| `transcript` | The `Transcript` to extract messages from. | +| `messages_as_str` | Rendering function from `message_numbering()`. | +| `model` | Model used for token counting and context window lookup. | +| `context_window` | Override the model's detected context window size (in tokens). | +| `compaction` | How to handle compaction boundaries: `"all"` (default) merges across boundaries; `"last"` uses only the final segment. | +| `depth` | Maximum depth of the span tree to process (timelines only). `None` recurses without limit. | +| `include_scorers` | Whether to include scorer events in extraction (default `False`). | + +: {tbl-colwidths=\[25,75\]} + + +### `segment_messages()` + +When you have a source other than a full `Transcript`---a list of messages, a list of events, or a `TimelineSpan`---use `segment_messages()` to render and split them into context-window-sized segments: + +``` python +from inspect_scout import message_numbering, segment_messages + +messages_as_str, extract_refs = message_numbering() + +async for segment in segment_messages( + source, # list[ChatMessage], list[Event], or TimelineSpan + messages_as_str=messages_as_str, +): + # segment.messages -> list[ChatMessage] + # segment.messages_str -> pre-rendered numbered string + # segment.segment -> 0-based segment index + ... +``` + +When given events or a `TimelineSpan`, it delegates to `span_messages()` internally to extract and merge messages (handling compaction boundaries), then segments the result by token count. + +| | | +|------------------------------------|------------------------------------| +| `source` | A `list[ChatMessage]`, `list[Event]`, or `TimelineSpan`. | +| `messages_as_str` | Rendering function from `message_numbering()`. | +| `model` | Model used for token counting and context window lookup. | +| `context_window` | Override the model's detected context window size (in tokens). | +| `compaction` | How to handle compaction boundaries (passed to `span_messages()`). | + +: {tbl-colwidths=\[25,75\]} + +### `span_messages()` + +The lowest-level extraction function. It takes a `Timeline`, `TimelineSpan`, or raw `list[Event]`, extracts `ChatMessage` objects from `ModelEvent`s, and handles compaction boundaries---all synchronously, without rendering or segmentation: + +``` python +from inspect_scout import span_messages + +# From a TimelineSpan +messages = span_messages(span) + +# From a list of events +messages = span_messages(events, compaction="last") + +# Split into per-compaction-region lists +regions = span_messages(span, split_compactions=True) +``` + +| | | +|------------------------------------|------------------------------------| +| `source` | A `Timeline`, `TimelineSpan`, or `list[Event]`. | +| `compaction` | How to handle compaction boundaries: `"all"` (default) merges across boundaries; `"last"` uses only the final `ModelEvent`'s messages; an `int` keeps the last *N* compaction regions. | +| `split_compactions` | When `True`, returns `list[list[ChatMessage]]` with one inner list per compaction region instead of a flat merged list. | + +: {tbl-colwidths=\[25,75\]} + +Use `span_messages()` when you need raw messages without rendering or token-based segmentation---for example, to inspect message content directly, apply your own formatting, or count messages before deciding how to process them. + +## Context Chunking {#context-chunking} + +When a transcript's messages exceed the scanning model's context window, `transcript_messages()` and `segment_messages()` automatically split them into segments sized to fit within 80% of the model's available context. Each segment is scanned independently with the same prompt. Use the `context_window` parameter on `transcript_messages()` or `segment_messages()` to override the model's detected context window size. + +### Result Reduction + +Because context-window segmentation can produce multiple results from a single source, those per-segment results need to be combined using a reducer. The `ResultReducer` class provides static async reducers for this purpose. + +| Reducer | Behaviour | +|-------------|----------------| +| `ResultReducer.any` | Boolean OR---`True` if any result is `True`. | +| `ResultReducer.mean` | Arithmetic mean of numeric values. | +| `ResultReducer.median` | Median of numeric values. | +| `ResultReducer.mode` | Mode (most common) of numeric values. | +| `ResultReducer.max` | Maximum of numeric values. | +| `ResultReducer.min` | Minimum of numeric values. | +| `ResultReducer.union` | Union of list values, deduplicated, preserving order. | +| `ResultReducer.majority` | Most common value, with last-result tiebreaker. | +| `ResultReducer.last` | Returns the last result with merged auxiliary fields. | + +All reducers are async functions with signature `(list[Result]) -> Result`. + +### LLM-Based Reduction + +For cases where statistical reduction isn't sufficient, `ResultReducer.llm()` returns a reducer that uses an LLM to synthesize segment results. It automatically formats each segment's answer, value, and explanation into a prompt, asks the model to synthesize them, and returns a single combined result. It is the default reducer for `"string"` answer types. + +``` python +from inspect_scout import ResultReducer, llm_scanner, scanner + +@scanner() +def synthesized_analysis(): + return llm_scanner( + question="Summarize the agent's key decisions.", + answer="string", + reducer=ResultReducer.llm(model="openai/gpt-5"), + ) +``` + +You can pass a custom `prompt` to guide how the model combines segment results. The per-segment answers, values, and explanations are automatically appended after your prompt: + +``` python +@scanner() +def security_summary(): + return llm_scanner( + question="Identify any security concerns in the agent's actions.", + answer="string", + reducer=ResultReducer.llm( + prompt=( + "You are reviewing security findings from different " + "segments of a long agent conversation. Prioritize the " + "most severe issues, deduplicate overlapping findings, " + "and produce a concise summary ordered by severity." + ), + ), + ) +``` + +### Custom Reducers + +You can also write a custom reducer — any async function with the signature `(list[Result]) -> Result`. Each `Result` in the list has `.value`, `.answer`, `.explanation`, and `.references` from its segment: + +``` python +from inspect_scout import Result, llm_scanner, scanner + +async def worst_score(results: list[Result]) -> Result: + """Keep the result with the lowest numeric value.""" + return min(results, key=lambda r: r.value) + +@scanner() +def strictest_scan(): + return llm_scanner( + question="Rate the quality of the agent's output (1-10).", + answer="numeric", + reducer=worst_score, + ) +``` + +Note that result reduction applies only to context-window segments within a single source. When scanning timelines with multiple spans, each span produces its own result independently — these are collected into a `ResultSet` without reduction. diff --git a/docs/scanners.qmd b/docs/scanners.qmd index 438463ecc..a6edd205d 100644 --- a/docs/scanners.qmd +++ b/docs/scanners.qmd @@ -4,357 +4,61 @@ title: Scanners ## Overview -Scanners are the main unit of processing in Inspect Scout and can target a wide variety of content types. In this article we'll cover the basic scanning concepts, and then drill into creating scanners that target various types (`Transcript`, `ChatMessage`, or `Event`) as well as creating custom loaders which enable scanning of lists of events or messages. +Scanners are the main unit of processing in Inspect Scout and can target a wide variety of content types. In this article we'll cover using the high-level scanners provided with Scout ([LLM Scanner](#llm-scanner) and [Grep Scanner](#grep-scanner)) as well as the basics of creating custom scanners. -This article goes in depth on custom scanner development. If you are looking for a straightforward high-level way to create an LLM-based scanner see the [LLM Scanner](llm_scanner.qmd) documentation. +## Using Scanners -Notet that you can also use scanners directly as Inspect scorers (see [Scanners as Scorers](#scanners-as-scorers) for details). +We'll cover defining various types of scanners below—first though let's describe how scanners are used in Scout. -## Scanner Basics +Scanners are defined either in Python source file or Python package, and are passed as the first argument to `scout scan`. For example: -A `Scanner` is a function that takes a `ScannerInput` (typically a `Transcript`, but possibly an `Event`, `ChatMessage`, or list of events or messages) and returns a `Result`. - -The result includes a `value` which can be of any type—this might be `True` to indicate that something was found but might equally be a number to indicate a count. More elaborate scanner values (`dict` or `list`) are also possible. - -Here is a simple scanner that uses a model to look for agent "confusion"—whether or not it finds confusion, it still returns the model completion as an `explanation`: - -``` python -@scanner(messages="all") -def confusion() -> Scanner[Transcript]: - - async def scan(transcript: Transcript) -> Result: - - # call model - output = await get_model().generate( - "Here is a transcript of an LLM agent " + - "solving a puzzle:\n\n" + - "===================================" + - await messages_as_str(transcript) + - "===================================\n\n" + - "In the transcript above do you see the " + - "agent becoming confused? Respond " + - "beginning with 'Yes' or 'No', followed " + - "by an explanation." - ) - - # extract the first word - match = re.match(r"^\w+", output.completion.strip()) - - # return result - if match: - answer = match.group(0) - return Result( - value=answer.lower() == "yes", - answer=answer, - explanation=output.completion, - ) - else: - return Result(value=False, explanation=output.completion) - - return scan -``` - -This scanner illustrates some of the lower-level mechanics of building custom scanners. You can also use the higher level `llm_scanner()` to implement this in far fewer lines of code: - -``` python -from inspect_scout import Transcript, llm_scanner, scanner - -@scanner(messages="all") -def confusion() -> Scanner[Transcript]: - return llm_scanner( - question="In the transcript above do you see " + - "the agent becoming confused?" - answer="boolean" - ) -``` - -### Input Types - -`Transcript` is the most common `ScannerInput` however several other types are possible: - -- `Event` — Single event from the transcript (e.g. `ModelEvent`, `ToolEvent`, etc.). - -- `ChatMessage` — Single chat message from the transcript message history. - -- `list[Event]` or `list[ChatMessage]` — Arbitrary sets of events or messages extracted from the `Transcript` (see [Loaders](#loaders) below for details). - -See the sections on [Transcripts](#transcripts), [Events](#events), [Messages](#messages), and [Loaders](#loaders) below for additional details on handling various input types. - -### Input Filtering - -One important principle of the Inspect Scout transcript pipeline is that only the precise data to be scanned should be read, and nothing more. This can dramatically improve performance as messages and events that won't be seen by scanners are never deserialized. Scanner input filters are specified as arguments to the `@scanner` decorator (you may have noticed the `messages="all"` attached to the scanner decorator in the example above). - -For example, here we are looking for instances of assistants swearing---for this task we only need to look at assistant messages so we specify `messages=["assistant"]` - -``` python -@scanner(messages=["assistant"]) -def assistant_swearing() -> Scanner[Transcript]: - - async def scan(transcript: Transcript) -> Result: - swear_words = [ - word - for m in transcript.messages - for word in extract_swear_words(m.text) - ] - return Result( - value=len(swear_words), - explanation=",".join(swear_words) - ) - - return scan -``` - -With this filter, only assistant messages (and no events at all) will be loaded from transcripts during scanning. - -Note that by default, no filters are active, so if you don't specify values for `messages` and/or `events` your scanner will not be called! - -## Transcripts {#transcripts} - -Transcripts are the most common input to scanners. If you are reading from Inspect eval logs, each log will have `samples * epochs` transcripts. If you are reading from another source, each agent trace will yield a single `Transcript`. - -### Transcript Fields - -{{< include _transcript_fields.md >}} - -### Content Filtering - -Note that the `messages` and `events` fields will not be populated unless you specify a `messages` or `events` filter on your scanner. For example, this scanner will see all messages and events: - -``` python -@scanner(messages="all", events="all") -def my_scanner() -> Scanner[Transcript]: ... -``` - -This scanner will see only model and tool events: - -``` python -@scanner(events=["model", "tool"]) -def my_scanner() -> Scanner[Transcript]: ... +``` bash +scout scan scanner.py -T ./logs --model openai/gpt-5 ``` -This scanner will see only assistant messages: +Or from Python: ``` python -@scanner(messages=["assistant"]) -def my_scanner() -> Scanner[Transcript]: ... -``` - -### Presenting Messages +from inspect_scout import scan +from scanner import my_scanner -When processing transcripts, you will often want to present an entire message history to model for analysis. Above, we used the `messages_as_str()` function to do this: - -``` python -# call model -result = await get_model().generate( - "Here is a transcript of an LLM agent " + - "solving a puzzle:\n\n" + - "===================================" + - await messages_as_str(transcript) + - "===================================\n\n" + - "In the transcript above do you see the agent " + - "becoming confused? Respond beginning with 'Yes' " + - "or 'No', followed by an explanation." +results = scan( + scanners=[my_scanner()], + transcripts="./logs", + model="openai/gpt-5" ) ``` -The `messages_as_str()` function takes a `Transcript | list[ChatMessage]` and will by default remove system messages from the message list. See `MessagesPreprocessor` for other available options. - -## Multiple Results {#multiple-results} - -Scanners can return multiple results as a list. For example: - -``` python -return [ - Result(label="deception", value=True, explanation="..."), - Result(label="misconfiguration", value=True, explanation="...") -] -``` - -This is useful when a scanner is capable of making several types of observation. In this case it's also important to indicate the origin of the result (i.e. which class of observation is is), which you can do using the `label` field (note that `label` can repeat multiple times in a set, so e.g. you could have multiple results with `label="deception"`). - -When a list is returned, each individual result will yield its own row in the [results data frame](results.qmd#data-frames). - -When validating scanners that return lists of results, you can use [result set validation](validation.qmd#result-set-validation) to specify expected values for each label independently. - -## Event Scanners {#events} - -To write a scanner that targets events, write a function that takes the event type(s) you want to process. For example, this scanner will see only model events: - -``` python -@scanner -def my_scanner() -> Scanner[ModelEvent]: - def scan(event: ModelEvent) -> Result: - ... - - return scan -``` - -Note that the `events="model"` filter was not required since we had already declared our scanner to take only model events. If we wanted to take both model and tool events we'd do this: - -``` python -@scanner -def my_scanner() -> Scanner[ModelEvent | ToolEvent]: - def scan(event: ModelEvent | ToolEvent) -> Result: - ... - - return scan -``` - -## Message Scanners {#messages} - -To write a scanner that targets messages, write a function that takes the message type(s) you want to process. For example, this scanner will only see tool messages: - -``` python -@scanner -def my_scanner() -> Scanner[ChatMessageTool]: - def scan(message: ChatMessageTool) -> Result: - ... - - return scan -``` - -This scanner will see only user and assistant messages: - -``` python -@scanner -def my_scanner() -> Scanner[ChatMessageUser | ChatMessageAssistant]: - def scan(message: ChatMessageUser | ChatMessageAssistant) -> Result: - ... - - return scan -``` - -## Scanner Metrics +Once a scan is complete you can view its results by running `scout view` or by computing on the data frame(s) returned in `results`. -{{< include _scanner_metrics.md >}} +Note that if you want run multiple scanners at once you can either pass a list of scanners to the `scan()` function or define a scan job (see [Scan Jobs](#scan-jobs) below for details). +## LLM Scanner -### Result Sets +{{< include _llm_scanner.md >}} -If your scanner yields [multiple results](#multiple-results) you can still use it as a scorer, but you will want to provide a dictionary of metrics corresponding to the labels used by your results. For example, if you have a scanner that can yield results with `label="deception"` or `label="misconfiguration"`, you might declare your metrics like this: -``` python -@scanner(messages="all", metrics=[{ "deception": [mean(), stderr()], "misconfiguration": [mean(), stderr()] }]) -def my_scanner() -> Scanner[Transcript]: ... -``` +## Grep Scanner -Or you can use a glob (\*) to use the same metrics for all labels: +{{< include _grep_scanner.md >}} -``` python -@scanner(messages="all", metrics=[{ "*": [mean(), stderr()] }]) -def my_scanner() -> Scanner[Transcript]: ... -``` -You should also be sure to return a result for each supported label (so that metrics can be computed correctly on each row). +## Custom Scanners +{{< include _custom_scanner.md >}} -## Scanners as Scorers {#scanners-as-scorers} +## Scan Jobs -You may have noticed that scanners are very similar to Inspect [Scorers](https://inspect.aisi.org.uk/scorers.html). This is by design, and it is actually possible to use scanners directly as Inspect scorers. +{{< include _scan_jobs.md >}} -For example, for the `confusion()` scorer we implemented above: +## Learning More -``` python -@scanner(messages="all") -def confusion() -> Scanner[Transcript]: - - async def scan(transcript: Transcript) -> Result: - - # model call eluded for brevity - output = get_model(...) +To learn more about using and developing scanners, see the following articles: - # extract the first word - match = re.match(r"^\w+", output.completion.strip()) +- [LLM Scanner](llm_scanner.qmd): High level scanner for using models to read transcripts. - # return result - if match: - answer = match.group(0) - return Result( - value=answer.lower() == "yes", - answer=answer, - explanation=output.completion, - ) - else: - return Result(value=False, explanation=output.completion) - - return scan -``` - -We can use this directly in an Inspect `Task` as follows: - -``` python -from .scanners import confusion - -@task -def mytask(): - return Task( - ..., - scorer = confusion() - ) -``` - -We can also use it with the `inspect score` command: - -``` bash -inspect score --scorer scanners.py@confusion logfile.eval -``` - -### Metrics - -The metrics used for the scorer will default to `mean()` and `stderr()`---however, you can also explicitly specify metrics on the `@scanner` decorator: - -``` python -@scanner(messages="all", metrics=[mean(), bootstrap_stderr()]) -def confusion() -> Scanner[Transcript]: ... -``` - -If you are interfacing with code that expects only `Scorer` instances, you can also use the `as_scorer()` function from Inspect Scout to explicitly convert your scanner to a scorer: - -``` python -from inspect_ai import eval -from inspect_scout import as_scorer - -from .mytasks import ctf_task -from .scanners import confusion - -eval(ctf_task(scorer=as_scorer(confusion()))) -``` - -If your scanner yields [multiple results](#multiple-results) see the discussion above on [Result Sets](#result-sets) for details on how to specify metrics for this case. - -## Custom Loaders {#loaders} - -When you want to process multiple discrete items from a `Transcript` this might not always fall neatly into single messages or events. For example, you might want to process pairs of user/assistant messages. To do this, create a custom `Loader` that yields the content as required. - -For example, here is a `Loader` that yields user/assistant message pairs: - -``` python -@loader(messages=["user", "assistant"]) -def conversation_turns(): - async def load( - transcript: Transcript - ) -> AsyncIterator[list[ChatMessage], None]: - - for user,assistant in message_pairs(transcript.messages): - yield [user, assistant] - - return load -``` - -Note that just like with scanners, the loader still needs to provide a `messages=["user", "assistant"]` in order to see those messages. - -We can now use this loader in a scanner that looks for refusals: - -``` python -@scanner(loader=conversation_turns()) -def assistant_refusals() -> Scanner[list[ChatMessage]]: +- [Grep Scanner](grep_scanner.qmd): High level scanner for pattern matching in transcripts. - async def scan(messages: list[ChatMessage]) -> Result: - user, assistant = messages - return Result( - value=is_refusal(assistant.text), - explanation=messages_as_str(messages) - ) +- [Custom Scanners](custom_scanner.qmd): Comprehensive documentation on creating custom scanners. - return scan -``` \ No newline at end of file +- [Scanner Tools](scanner_tools.qmd): Create custom scanners with the same tools used under the hood by `llm_scanner()`. diff --git a/docs/scripts/post-render.sh b/docs/scripts/post-render.sh index d547f7248..8c132ffb6 100755 --- a/docs/scripts/post-render.sh +++ b/docs/scripts/post-render.sh @@ -1,6 +1,6 @@ #!/bin/bash -files=("index" "examples" "workflow" "projects" "transcripts" "scanners" "llm_scanner" "grep_scanner" "results" "validation" "db_overview" "db_schema" "db_capturing" "db_importing" "db_publishing" "reference/scanning" "reference/transcript" "reference/scanner" "reference/async") +files=("index" "examples" "workflow" "projects" "transcripts" "scanners" "results" "validation" "llm_scanner" "grep_scanner" "custom_scanner" "scanner_tools" "multi_agent" "db_overview" "db_schema" "db_capturing" "db_importing" "db_publishing" "reference/scanning" "reference/transcript" "reference/scanner" "reference/async") if [ "$QUARTO_PROJECT_RENDER_ALL" = "1" ]; then diff --git a/examples/cc-import/.gitignore b/examples/cc-import/.gitignore new file mode 100644 index 000000000..810e3627c --- /dev/null +++ b/examples/cc-import/.gitignore @@ -0,0 +1,3 @@ +scans/ +transcripts/ + diff --git a/examples/sources/transcripts/.gitignore b/examples/sources/transcripts/.gitignore deleted file mode 100644 index b11ed9fb6..000000000 --- a/examples/sources/transcripts/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -_index/ -*.parquet diff --git a/examples/validation-demo/refusal.csv b/examples/validation-demo/refusal.csv index de57b87f5..4fa77da69 100644 --- a/examples/validation-demo/refusal.csv +++ b/examples/validation-demo/refusal.csv @@ -18,3 +18,4 @@ kwdhBbmTzuLG9cadLqREYS,False, 7ukAnKVG2kjy3iQkxk4PdU,False, 8DzFVEeDdgXWr4wSv3o5qh,False, 3t2pHknZ8HbgFNi4bAnJMM,stop,icontains +DrBupuwbxtGY4Tgm7K48CV,true, diff --git a/pyproject.toml b/pyproject.toml index 886fdd709..4a7eea87a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,11 +17,12 @@ dependencies = [ "click>=8.1.3,!=8.2.0,<8.2.2", "cloudpickle>=3.0.0", "dill>=0.4.0", - "duckdb>=1.4.0", + "duckdb>=1.4.0,<1.5", "fastapi>=0.128.1", "ijson>=3.2.0", "importlib-metadata", - "inspect-ai>=0.3.179", + "inspect-ai>=0.3.196", + "inspect-swe>=0.2.42", "jinja2>=3.0.0", "jsonlines>=3.0.0", "jsonschema>3.1.1", @@ -57,13 +58,13 @@ dev = [ # zipfile-zstd patches zipfile to support zstd compression (for creating test fixtures) "zipfile-zstd>=0.0.4; python_version < '3.14'", "pytest-xdist", - "anthropic", + "anthropic>=0.80.0", "arize-phoenix-client", - "google-genai", + "google-genai>=1.56.0", "huggingface_hub", "langsmith", "logfire", - "openai", + "openai>=2.17.0", "pytest-dotenv", "pytest-asyncio", "mypy", @@ -118,7 +119,7 @@ dist = ["twine", "build"] [tool.hatch.build] only-packages = true exclude = ["docs/", "tests/"] -artifacts = ["src/inspect_scout/_view/www/dist"] +artifacts = ["src/inspect_scout/_view/dist"] [tool.hatch.version] source = "vcs" diff --git a/scripts/export_openapi_schema.py b/scripts/export_openapi_schema.py index 9b22a1313..288eb6295 100644 --- a/scripts/export_openapi_schema.py +++ b/scripts/export_openapi_schema.py @@ -29,8 +29,8 @@ def main() -> None: app = v2_api_app() schema = app.openapi() - # Write to www/openapi.json - output_path = repo_root / "src/inspect_scout/_view/www/openapi.json" + # Write to _view/openapi.json (read by the TS monorepo's generate-types script) + output_path = repo_root / "src/inspect_scout/_view/openapi.json" output_path.parent.mkdir(parents=True, exist_ok=True) with output_path.open("w") as f: diff --git a/src/inspect_scout/__init__.py b/src/inspect_scout/__init__.py index c5256fb65..e32415156 100644 --- a/src/inspect_scout/__init__.py +++ b/src/inspect_scout/__init__.py @@ -1,7 +1,18 @@ from inspect_ai._util.deprecation import relocated_module_attribute from ._grep_scanner import grep_scanner -from ._llm_scanner import AnswerMultiLabel, AnswerStructured, llm_scanner +from ._llm_scanner import ( + Answer, + AnswerMultiLabel, + AnswerSpec, + AnswerStructured, + ResultReducer, + answer_type, + generate_answer, + llm_scanner, + parse_answer, + scanner_prompt, +) from ._observe import ObserveEmit, ObserveProvider, observe, observe_update from ._project import ProjectConfig from ._query.condition import Condition @@ -22,6 +33,7 @@ from ._scanner.extract import ( MessageFormatOptions, MessagesPreprocessor, + message_numbering, messages_as_str, tool_callers, ) @@ -50,12 +62,23 @@ from ._transcript.database.schema import transcripts_db_schema from ._transcript.factory import transcripts_from from ._transcript.log import LogColumns, log_columns +from ._transcript.messages import ( + MessagesSegment, + segment_messages, + span_messages, + transcript_messages, +) from ._transcript.sample_metadata import SampleMetadata +from ._transcript.timeline import ( + TimelineMessages, + timeline_messages, +) from ._transcript.transcripts import ScannerWork, Transcripts, TranscriptsReader from ._transcript.types import ( EventType, MessageType, Transcript, + TranscriptContent, TranscriptInfo, ) from ._util.refusal import RefusalError @@ -120,6 +143,9 @@ "LogColumns", "log_columns", "SampleMetadata", + # timeline + "TimelineMessages", + "timeline_messages", # scanner "Error", "Scanner", @@ -131,15 +157,28 @@ "loader", "EventType", "MessageType", + "TranscriptContent", "as_scorer", + "message_numbering", "messages_as_str", + "span_messages", + "segment_messages", + "MessagesSegment", + "transcript_messages", "MessageFormatOptions", "MessagesPreprocessor", "tool_callers", "RefusalError", "llm_scanner", + "ResultReducer", + "Answer", "AnswerMultiLabel", + "AnswerSpec", "AnswerStructured", + "answer_type", + "generate_answer", + "parse_answer", + "scanner_prompt", "grep_scanner", # validation "ValidationSet", diff --git a/src/inspect_scout/_cli/common.py b/src/inspect_scout/_cli/common.py index c6b351a93..05266aa05 100644 --- a/src/inspect_scout/_cli/common.py +++ b/src/inspect_scout/_cli/common.py @@ -96,6 +96,13 @@ def view_options(func: Callable[..., Any]) -> Callable[..., click.Context]: default=None, help="Open in web browser.", ) + @click.option( + "--root-path", + type=str, + default="", + envvar="UVICORN_ROOT_PATH", + help="ASGI root_path for serving behind a reverse proxy.", + ) @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> click.Context: return cast(click.Context, func(*args, **kwargs)) diff --git a/src/inspect_scout/_cli/db.py b/src/inspect_scout/_cli/db.py index b12401398..006e45537 100644 --- a/src/inspect_scout/_cli/db.py +++ b/src/inspect_scout/_cli/db.py @@ -1,19 +1,10 @@ import asyncio import json -import sys from pathlib import Path import click import duckdb -from inspect_ai._util.error import PrerequisiteError -from inspect_scout._transcript.database.parquet.encryption import ( - ENCRYPTION_KEY_ENV, - decrypt_database, - encrypt_database, - get_encryption_key_from_env, - validate_encryption_key, -) from inspect_scout._transcript.database.parquet.index import create_index from inspect_scout._transcript.database.parquet.types import IndexStorage from inspect_scout._transcript.database.schema import ( @@ -22,135 +13,24 @@ ) -def _resolve_key(key: str | None) -> str: - """Resolve encryption key from CLI option, stdin, or environment. - - Args: - key: Key value from --key option (may be "-" for stdin, None if not provided) - - Returns: - The resolved encryption key. - - Raises: - PrerequisiteError: If no key is available or key is invalid. - """ - resolved_key: str - if key == "-": - # Read from stdin - resolved_key = sys.stdin.read().strip() - elif key is not None: - resolved_key = key - else: - # Try environment variable - env_key = get_encryption_key_from_env() - if env_key: - resolved_key = env_key - else: - raise PrerequisiteError( - f"No encryption key provided. Use --key or set {ENCRYPTION_KEY_ENV}" - ) - - # Validate the key - try: - validate_encryption_key(resolved_key) - except ValueError as e: - raise PrerequisiteError(str(e)) from e - - return resolved_key - - @click.group("db") def db_command() -> None: """Scout transcript database management.""" return None -@db_command.command("encrypt") -@click.argument("database-location", type=str, required=True) -@click.option( - "--output-dir", - required=True, - help="Directory to write encrypted database files to.", -) -@click.option( - "--key", - type=str, - default=None, - envvar="SCOUT_DB_ENCRYPTION_KEY", - help="Encryption key (use '-' for stdin, or set SCOUT_DB_ENCRYPTION_KEY).", -) -@click.option( - "--overwrite", - type=bool, - is_flag=True, - default=False, - help="Overwrite files in the output directory.", -) -def encrypt( - database_location: str, output_dir: str, key: str | None, overwrite: bool -) -> None: - """Encrypt a transcript database.""" - resolved_key = _resolve_key(key) - encrypt_database(database_location, output_dir, resolved_key, overwrite) - - -@db_command.command("decrypt") -@click.argument("database-location", type=str, required=True) -@click.option( - "--output-dir", - required=True, - help="Directory to write decrypted database files to.", -) -@click.option( - "--key", - type=str, - default=None, - envvar="SCOUT_DB_ENCRYPTION_KEY", - help="Encryption key (use '-' for stdin, or set SCOUT_DB_ENCRYPTION_KEY).", -) -@click.option( - "--overwrite", - type=bool, - is_flag=True, - default=False, - help="Overwrite files in the output directory.", -) -def decrypt( - database_location: str, output_dir: str, key: str | None, overwrite: bool -) -> None: - """Decrypt a transcript database.""" - resolved_key = _resolve_key(key) - decrypt_database(database_location, output_dir, resolved_key, overwrite) - - @db_command.command("index") @click.argument("database-location", type=str, required=True) -@click.option( - "--key", - type=str, - default=None, - help="Encryption key for encrypted databases (use '-' for stdin, or set SCOUT_DB_ENCRYPTION_KEY).", -) -def index(database_location: str, key: str | None) -> None: +def index(database_location: str) -> None: """Create or rebuild the index for a transcript database. This scans all parquet data files and creates a manifest index containing metadata for fast queries. Any existing index files are replaced. - - For encrypted databases, provide --key or set SCOUT_DB_ENCRYPTION_KEY. """ - # Resolve key if provided (handles stdin), but don't require it - # IndexStorage.create() will error if encrypted files detected without key - try: - resolved_key = _resolve_key(key) if key is not None else None - except PrerequisiteError: - resolved_key = None async def _run() -> None: - storage = await IndexStorage.create( - location=database_location, key=resolved_key - ) + storage = IndexStorage.create(location=database_location) conn = duckdb.connect(":memory:") try: result = await create_index(conn, storage) @@ -214,46 +94,21 @@ def schema(fmt: str, output: str | None) -> None: @db_command.command("validate") @click.argument("database-location", type=str, required=True) -@click.option( - "--key", - type=str, - default=None, - help="Encryption key for encrypted databases (use '-' for stdin, or set SCOUT_DB_ENCRYPTION_KEY).", -) -def validate(database_location: str, key: str | None) -> None: +def validate(database_location: str) -> None: """Validate a transcript database schema. Checks that the database has the required fields and correct types. Examples: scout db validate ./my_transcript_db - - scout db validate ./encrypted_db --key $KEY """ - import tempfile - path = Path(database_location) if not path.exists(): click.echo(f"Error: Path does not exist: {database_location}", err=True) raise SystemExit(1) - # Check if database appears to be encrypted (has .enc files) - enc_files = list(path.glob("*.parquet.enc")) - parquet_files = list(path.glob("*.parquet")) - - if enc_files and not parquet_files: - # Database is encrypted - need key to validate - resolved_key = _resolve_key(key) - - # Decrypt to temp directory for validation - with tempfile.TemporaryDirectory() as temp_dir: - click.echo("Decrypting database for validation...") - decrypt_database(database_location, temp_dir, resolved_key, overwrite=False) - errors = validate_transcript_schema(Path(temp_dir)) - else: - # Database is not encrypted - errors = validate_transcript_schema(path) + errors = validate_transcript_schema(path) if errors: click.echo("Schema validation failed:", err=True) diff --git a/src/inspect_scout/_cli/import_command.py b/src/inspect_scout/_cli/import_command.py new file mode 100644 index 000000000..25c0ace94 --- /dev/null +++ b/src/inspect_scout/_cli/import_command.py @@ -0,0 +1,419 @@ +"""Scout import CLI command. + +Imports transcripts from registered sources into a local transcript database. +""" + +import asyncio +import importlib +import inspect +from datetime import datetime +from pathlib import Path +from typing import Any, Callable + +import click +import yaml +from typing_extensions import Unpack + +from inspect_scout._cli.common import ( + CommonOptions, + common_options, + process_common_options, +) +from inspect_scout._display._display import display +from inspect_scout._util.constants import DEFAULT_TRANSCRIPTS_DIR + + +def _discover_sources() -> dict[str, Callable[..., Any]]: + """Discover available source functions from inspect_scout.sources. + + Returns: + Dict mapping source name to source function. + """ + sources_module = importlib.import_module("inspect_scout.sources") + source_names: list[str] = getattr(sources_module, "__all__", []) + sources: dict[str, Callable[..., Any]] = {} + for name in sorted(source_names): + fn = getattr(sources_module, name, None) + if fn is not None and callable(fn): + sources[name] = fn + return sources + + +def _get_type_name(annotation: Any) -> str: + """Extract a readable type name from a type annotation.""" + if annotation is inspect.Parameter.empty: + return "" + # Handle union types (e.g., str | None, list[str] | None) + origin = getattr(annotation, "__origin__", None) + args = getattr(annotation, "__args__", ()) + + # Handle UnionType (X | Y) + if origin is not None and str(origin) == "typing.Union": + # Filter out NoneType + non_none = [a for a in args if a is not type(None)] + if len(non_none) == 1: + return _get_type_name(non_none[0]) + return " | ".join(_get_type_name(a) for a in non_none) + + # Python 3.10+ union syntax (types.UnionType) + import types + + if isinstance(annotation, types.UnionType): + non_none = [a for a in args if a is not type(None)] + if len(non_none) == 1: + return _get_type_name(non_none[0]) + return " | ".join(_get_type_name(a) for a in non_none) + + # Handle generic types like list[str], dict[str, str] + if origin is not None: + origin_name = getattr(origin, "__name__", str(origin)) + if args: + arg_names = ", ".join(_get_type_name(a) for a in args) + return f"{origin_name}[{arg_names}]" + return origin_name + + # Handle PathLike + if hasattr(annotation, "__name__"): + return str(annotation.__name__) + return str(annotation) + + +def _print_sources(sources: dict[str, Callable[..., Any]]) -> None: + """Print available sources and their parameters.""" + d = display() + d.print("\nAvailable sources:\n") + for name, fn in sources.items(): + d.print(f" [bold]{name}[/bold]", markup=True) + sig = inspect.signature(fn) + for param_name, param in sig.parameters.items(): + type_name = _get_type_name(param.annotation) + d.print(f" {param_name:<20s}{type_name}") + d.print("") + + +def _has_datetime_annotation(annotation: Any) -> bool: + """Check if a parameter annotation includes datetime.""" + # Handle string annotations (from `from __future__ import annotations`) + if isinstance(annotation, str): + parts = [p.strip() for p in annotation.split("|")] + return "datetime" in parts + if annotation is datetime: + return True + args = getattr(annotation, "__args__", ()) + return any(a is datetime for a in args) + + +def _has_int_annotation(annotation: Any) -> bool: + """Check if a parameter annotation includes int.""" + # Handle string annotations (from `from __future__ import annotations`) + if isinstance(annotation, str): + parts = [p.strip() for p in annotation.split("|")] + return "int" in parts + if annotation is int: + return True + args = getattr(annotation, "__args__", ()) + return any(a is int for a in args) + + +def _is_str_only_annotation(annotation: Any) -> bool: + """Check if annotation is strictly str or str | None (no other types).""" + import types + + if isinstance(annotation, str): + parts = [p.strip() for p in annotation.split("|")] + return parts == ["str"] or set(parts) == {"str", "None"} + if annotation is str: + return True + # Only check args for union types (not e.g. list[str]) + origin = getattr(annotation, "__origin__", None) + is_union = isinstance(annotation, types.UnionType) or ( + origin is not None and str(origin) == "typing.Union" + ) + if not is_union: + return False + args = getattr(annotation, "__args__", ()) + non_none = [a for a in args if a is not type(None)] + return len(non_none) == 1 and non_none[0] is str + + +def _coerce_value(value: str, annotation: Any) -> Any: + """Coerce a string value based on the parameter's type annotation.""" + if _has_datetime_annotation(annotation): + return datetime.fromisoformat(value) + if _has_int_annotation(annotation): + return int(value) + # String-annotated params should stay as strings (YAML parsing would + # coerce values like "123" or "true" into int/bool). + if _is_str_only_annotation(annotation): + return value + # Use YAML parsing for everything else (handles lists, dicts, bools) + try: + parsed = yaml.safe_load(value) + if parsed is not None: + return parsed + return value + except yaml.YAMLError: + return value + + +def _parse_params( + source_fn: Callable[..., Any], + params: tuple[str, ...], + limit: int | None, + from_time: str | None, + to_time: str | None, +) -> dict[str, Any]: + """Parse -P key=value pairs and promoted params into kwargs for a source function. + + Args: + source_fn: The source function to call. + params: Tuple of "key=value" strings from -P options. + limit: Promoted --limit value. + from_time: Promoted --from value. + to_time: Promoted --to value. + + Returns: + Dict of kwargs to pass to the source function. + + Raises: + click.UsageError: If a parameter is unknown or value parsing fails. + """ + sig = inspect.signature(source_fn) + valid_params = set(sig.parameters.keys()) + + # Parse -P key=value pairs + kwargs: dict[str, Any] = {} + for param_str in params: + if "=" not in param_str: + raise click.UsageError( + f"Invalid parameter format: '{param_str}'. Expected 'name=value'." + ) + key, value = param_str.split("=", 1) + if key not in valid_params: + valid_list = ", ".join(sorted(valid_params)) + raise click.UsageError( + f"Unknown parameter '{key}' for source. Valid parameters: {valid_list}" + ) + annotation = sig.parameters[key].annotation + try: + kwargs[key] = _coerce_value(value, annotation) + except (ValueError, TypeError) as e: + raise click.UsageError(f"Invalid value for '{key}': {e}") from e + + # Merge promoted params (take precedence over -P) + if limit is not None: + if "limit" in valid_params: + kwargs["limit"] = limit + else: + raise click.UsageError("This source does not accept a 'limit' parameter.") + + if from_time is not None: + if "from_time" in valid_params: + try: + kwargs["from_time"] = datetime.fromisoformat(from_time) + except ValueError as e: + raise click.UsageError(f"Invalid --from value: {e}") from e + else: + raise click.UsageError( + "This source does not accept a 'from_time' parameter." + ) + + if to_time is not None: + if "to_time" in valid_params: + try: + kwargs["to_time"] = datetime.fromisoformat(to_time) + except ValueError as e: + raise click.UsageError(f"Invalid --to value: {e}") from e + else: + raise click.UsageError("This source does not accept a 'to_time' parameter.") + + return kwargs + + +async def _run_import( + source_fn: Callable[..., Any], + source_name: str, + kwargs: dict[str, Any], + transcripts_dir: str, +) -> None: + """Execute the import: call source function and write to database.""" + from inspect_scout._transcript.database.factory import transcripts_db + + d = display() + d.print(f"\nImporting from [bold]{source_name}[/bold]...\n", markup=True) + + async with transcripts_db(transcripts_dir) as db: + await db.insert(source_fn(**kwargs)) + + d.print("\nImport complete. To view transcripts:\n") + d.print(f" scout view -T {transcripts_dir}\n") + + +async def _run_dry_run( + source_fn: Callable[..., Any], + source_name: str, + kwargs: dict[str, Any], +) -> None: + """Execute a dry run: fetch transcripts and display summary without writing.""" + from inspect_scout._transcript.types import Transcript + + d = display() + d.print(f"\nDry run — fetching from [bold]{source_name}[/bold]...\n", markup=True) + + transcripts: list[Transcript] = [] + async_iter = source_fn(**kwargs) + async for transcript in async_iter: + transcripts.append(transcript) + + if not transcripts: + d.print("No transcripts found.") + return + + # Print summary table + d.print(f"{'ID':<40s} {'Date':<12s} {'Model':<25s} {'Messages':>8s} {'Tokens':>8s}") + d.print("-" * 95) + for t in transcripts: + date_str = t.date[:10] if t.date else "-" + model_str = (t.model or "-")[:25] + msg_count = str(t.message_count) if t.message_count is not None else "-" + token_count = str(t.total_tokens) if t.total_tokens is not None else "-" + d.print( + f"{t.transcript_id[:40]:<40s} {date_str:<12s} {model_str:<25s} " + f"{msg_count:>8s} {token_count:>8s}" + ) + + d.print(f"\n{len(transcripts)} transcript(s) found.") + + +@click.command("import") +@click.argument("source", required=False, default=None) +@click.option( + "-T", + "--transcripts", + type=str, + default=DEFAULT_TRANSCRIPTS_DIR, + help="Transcripts database directory.", +) +@click.option( + "--limit", + type=int, + default=None, + help="Maximum number of transcripts to import.", +) +@click.option( + "--from", + "from_time", + type=str, + default=None, + help="Only import transcripts on or after this time (ISO 8601).", +) +@click.option( + "--to", + "to_time", + type=str, + default=None, + help="Only import transcripts before this time (ISO 8601).", +) +@click.option( + "-P", + "params", + type=str, + multiple=True, + help="Source parameter as name=value (repeatable).", +) +@click.option( + "--sources", + is_flag=True, + default=False, + help="List available sources and their parameters.", +) +@click.option( + "--dry-run", + is_flag=True, + default=False, + help="Fetch and display summary without writing.", +) +@click.option( + "--overwrite", + is_flag=True, + default=False, + help="Overwrite existing transcripts directory without prompting.", +) +@common_options +def import_command( + source: str | None, + transcripts: str, + limit: int | None, + from_time: str | None, + to_time: str | None, + params: tuple[str, ...], + sources: bool, + dry_run: bool, + overwrite: bool, + **common: Unpack[CommonOptions], +) -> None: + """Import transcripts from a source.""" + process_common_options(common) + + available_sources = _discover_sources() + + # Handle --sources flag + if sources: + _print_sources(available_sources) + return + + # Require source name if not listing + if source is None: + raise click.UsageError( + "Missing source name. Use 'scout import --sources' to see available sources." + ) + + # Validate source name + if source not in available_sources: + source_list = ", ".join(sorted(available_sources.keys())) + raise click.UsageError( + f"Unknown source '{source}'. Available sources: {source_list}" + ) + + source_fn = available_sources[source] + + # Parse parameters + kwargs = _parse_params(source_fn, params, limit, from_time, to_time) + + if dry_run: + asyncio.run(_run_dry_run(source_fn, source, kwargs)) + else: + # Check if transcripts directory exists + transcripts_path = Path(transcripts) + if transcripts_path.exists(): + if overwrite: + _remove_transcripts_dir(transcripts_path) + else: + from rich.prompt import Prompt + + choice = Prompt.ask( + f"\nTranscripts directory '{transcripts}' already exists\n" + " [bold]1[/bold]) Add transcripts (existing transcripts won't be re-imported)\n" + " [bold]2[/bold]) Overwrite (delete existing transcripts first)\n" + " [bold]3[/bold]) Cancel\n", + choices=["1", "2", "3"], + default="1", + ) + if choice == "3": + raise SystemExit(0) + if choice == "2": + _remove_transcripts_dir(transcripts_path) + + asyncio.run(_run_import(source_fn, source, kwargs, transcripts)) + + +def _remove_transcripts_dir(path: Path) -> None: + """Remove a transcripts directory, validating it is a real directory.""" + import shutil + + if not path.is_dir(): + raise click.UsageError(f"'{path}' exists but is not a directory.") + if path.is_symlink(): + path.unlink() + else: + shutil.rmtree(path) diff --git a/src/inspect_scout/_cli/main.py b/src/inspect_scout/_cli/main.py index 5387fb8fe..f364c137b 100644 --- a/src/inspect_scout/_cli/main.py +++ b/src/inspect_scout/_cli/main.py @@ -4,6 +4,7 @@ from .. import __version__ from .._scan import init_environment from .db import db_command +from .import_command import import_command from .info import info_command from .scan import scan_command from .scan_complete import scan_complete_command @@ -25,6 +26,7 @@ def scout() -> None: scan_command.add_command(scan_complete_command) scan_command.add_command(scan_list_command) scan_command.add_command(scan_status_command) +scout.add_command(import_command) scout.add_command(view_command) scout.add_command(trace_command) scout.add_command(info_command) diff --git a/src/inspect_scout/_cli/view.py b/src/inspect_scout/_cli/view.py index 200d374ce..0111942b5 100644 --- a/src/inspect_scout/_cli/view.py +++ b/src/inspect_scout/_cli/view.py @@ -48,6 +48,7 @@ def view_command( host: str, port: int, browser: bool | None, + root_path: str, **common: Unpack[CommonOptions], ) -> None: """View scan results.""" @@ -63,4 +64,5 @@ def view_command( mode=mode, authorization=resolve_view_authorization(), log_level=common["log_level"], + root_path=root_path, ) diff --git a/src/inspect_scout/_concurrency/_mp_common.py b/src/inspect_scout/_concurrency/_mp_common.py index e690acef8..811690455 100644 --- a/src/inspect_scout/_concurrency/_mp_common.py +++ b/src/inspect_scout/_concurrency/_mp_common.py @@ -36,6 +36,44 @@ from .common import ParseFunctionResult, ParseJob, ScanMetrics, ScannerJob +# --------------------------------------------------------------------------- +# copyreg reducer: make inspect_ai Model picklable via cloudpickle +# --------------------------------------------------------------------------- +# ModelAPI subclasses contain unpicklable state (anyio.Lock, httpx.AsyncClient, +# SDK async clients). When a scanner closure captures a Model instance, +# cloudpickle fails with "cannot pickle '_thread.RLock' object". +# +# We register a copyreg reducer that converts Model → ModelConfig for +# serialization, then reconstructs via get_model() on unpickle. get_model() +# uses memoize=True by default, so repeated unpickles in the same worker +# reuse the cached instance rather than creating duplicate HTTP clients. +# --------------------------------------------------------------------------- + +import copyreg + +from inspect_ai.model import get_model +from inspect_ai.model._model import Model +from inspect_ai.model._model_config import model_to_model_config + + +def _reconstruct_model(config: ModelConfig) -> Model: + """Reconstruct a Model from its picklable ModelConfig.""" + return get_model( + model=config.model, + config=config.config, + base_url=config.base_url, + **config.args, + ) + + +def _reduce_model(model: Model) -> tuple[Callable[..., Any], tuple[ModelConfig]]: + """Reduce a Model to picklable ModelConfig for cloudpickle serialization.""" + return (_reconstruct_model, (model_to_model_config(model),)) + + +copyreg.pickle(Model, _reduce_model) + + class DillCallable: """Wrapper for callables that uses dill for pickling. diff --git a/src/inspect_scout/_display/rich.py b/src/inspect_scout/_display/rich.py index 494f8b563..ae640af21 100644 --- a/src/inspect_scout/_display/rich.py +++ b/src/inspect_scout/_display/rich.py @@ -24,7 +24,7 @@ TimeElapsedColumn, TimeRemainingColumn, ) -from rich.table import Table +from rich.table import Column, Table from rich.text import Text from typing_extensions import override @@ -213,16 +213,33 @@ def __init__( ): self._caption = caption self._count = count - text_column_fmt = "[blue]{task.description}:[/blue] [meta]{task.fields[text]}" + + # Build count format string for a separate column + count_fmt = "" if self._count: - text_column_fmt = text_column_fmt + " - {task.completed:,}" + count_fmt = "{task.completed:,}" if not isinstance(self._count, bool): - text_column_fmt = text_column_fmt + "/{task.total:,}" + count_fmt = count_fmt + "/{task.total:,}" - text_column_fmt = text_column_fmt + "[/meta]" - self._progress = Progress(SpinnerColumn(), TextColumn(text_column_fmt)) + self._progress = Progress( + SpinnerColumn(), + TextColumn( + "[blue]{task.description}:[/blue]", + table_column=Column(no_wrap=True), + ), + TextColumn( + "[meta]{task.fields[text]}[/meta]", + table_column=Column(width=40, no_wrap=True), + ), + TextColumn(f"[meta]{count_fmt}[/meta]") if self._count else TextColumn(""), + TimeElapsedColumn(), + ) self._task_id = self._progress.add_task( - caption, total=count if isinstance(count, int) else None, text="(preparing)" + caption, + total=count + if isinstance(count, int) and not isinstance(count, bool) + else None, + text="(preparing)", ) self._started = False diff --git a/src/inspect_scout/_grep_scanner/_event.py b/src/inspect_scout/_grep_scanner/_event.py index 0f6ff6d20..f9db3176d 100644 --- a/src/inspect_scout/_grep_scanner/_event.py +++ b/src/inspect_scout/_grep_scanner/_event.py @@ -4,7 +4,15 @@ from logging import getLogger from inspect_ai._util.logger import warn_once -from inspect_ai.event import Event +from inspect_ai.event import ( + ApprovalEvent, + ErrorEvent, + Event, + InfoEvent, + LoggerEvent, + ModelEvent, + ToolEvent, +) logger = getLogger(__name__) @@ -42,107 +50,92 @@ def event_as_str(event: Event) -> str | None: def _model_event_as_str(event: Event) -> str | None: """Extract completion text from ModelEvent.""" - # ModelEvent has output.completion - if hasattr(event, "output") and event.output is not None: - completion = getattr(event.output, "completion", None) - if completion: - return f"MODEL:\n{completion}\n" + if not isinstance(event, ModelEvent): + return None + completion = event.output.completion + if completion: + return f"MODEL:\n{completion}\n" return None def _tool_event_as_str(event: Event) -> str | None: """Format ToolEvent with function, arguments, and result.""" - # ToolEvent has function, arguments, result, error - function = getattr(event, "function", "unknown") - arguments = getattr(event, "arguments", {}) - result = getattr(event, "result", None) - error = getattr(event, "error", None) + if not isinstance(event, ToolEvent): + return None - parts = [f"TOOL ({function}):"] + parts = [f"TOOL ({event.function}):"] - if arguments: - if isinstance(arguments, dict): - args_text = "\n".join(f" {k}: {v}" for k, v in arguments.items()) + if event.arguments: + if isinstance(event.arguments, dict): + args_text = "\n".join(f" {k}: {v}" for k, v in event.arguments.items()) parts.append(f"Arguments:\n{args_text}") else: - parts.append(f"Arguments: {arguments}") + parts.append(f"Arguments: {event.arguments}") - if result is not None: - result_str = str(result) if not isinstance(result, str) else result + if event.result is not None: + result_str = ( + str(event.result) if not isinstance(event.result, str) else event.result + ) parts.append(f"Result: {result_str}") - if error is not None: - error_msg = getattr(error, "message", str(error)) - parts.append(f"Error: {error_msg}") + if event.error is not None: + parts.append(f"Error: {event.error.message}") return "\n".join(parts) + "\n" def _error_event_as_str(event: Event) -> str | None: """Extract error message from ErrorEvent.""" - # ErrorEvent has error (EvalError with message) - error = getattr(event, "error", None) - if error is not None: - message = getattr(error, "message", str(error)) - return f"ERROR:\n{message}\n" - return None + if not isinstance(event, ErrorEvent): + return None + return f"ERROR:\n{event.error.message}\n" def _info_event_as_str(event: Event) -> str | None: """Format InfoEvent data as string or JSON.""" - # InfoEvent has source, data - source = getattr(event, "source", None) - data = getattr(event, "data", None) + if not isinstance(event, InfoEvent): + return None - if data is None: + if event.data is None: return None # Convert data to string - JSON dump if not already a string - if isinstance(data, str): - data_str = data + if isinstance(event.data, str): + data_str = event.data else: - data_str = json.dumps(data, default=str) + data_str = json.dumps(event.data, default=str) - source_part = f" ({source})" if source else "" + source_part = f" ({event.source})" if event.source else "" return f"INFO{source_part}:\n{data_str}\n" def _logger_event_as_str(event: Event) -> str | None: """Extract log message from LoggerEvent.""" - # LoggerEvent has message (LoggingMessage with message, level) - msg = getattr(event, "message", None) - if msg is not None: - level = getattr(msg, "level", "info") - message = getattr(msg, "message", str(msg)) - return f"LOG ({level}):\n{message}\n" - return None + if not isinstance(event, LoggerEvent): + return None + return f"LOG ({event.message.level}):\n{event.message.message}\n" def _approval_event_as_str(event: Event) -> str | None: """Format ApprovalEvent with message, tool call, and decision.""" - # ApprovalEvent has message, call (ToolCall), decision, explanation - message = getattr(event, "message", "") - call = getattr(event, "call", None) - decision = getattr(event, "decision", "unknown") - explanation = getattr(event, "explanation", None) - - parts = [f"APPROVAL ({decision}):"] - - if message: - parts.append(f"Message: {message}") - - if call is not None: - function = getattr(call, "function", "unknown") - arguments = getattr(call, "arguments", {}) - parts.append(f"Tool: {function}") - if arguments: - if isinstance(arguments, dict): - args_text = ", ".join(f"{k}={v}" for k, v in arguments.items()) - parts.append(f"Args: {args_text}") - else: - parts.append(f"Args: {arguments}") - - if explanation: - parts.append(f"Explanation: {explanation}") + if not isinstance(event, ApprovalEvent): + return None + + parts = [f"APPROVAL ({event.decision}):"] + + if event.message: + parts.append(f"Message: {event.message}") + + call = event.call + parts.append(f"Tool: {call.function}") + if call.arguments: + if isinstance(call.arguments, dict): + args_text = ", ".join(f"{k}={v}" for k, v in call.arguments.items()) + parts.append(f"Args: {args_text}") + else: + parts.append(f"Args: {call.arguments}") + + if event.explanation: + parts.append(f"Explanation: {event.explanation}") return "\n".join(parts) + "\n" diff --git a/src/inspect_scout/_llm_scanner/__init__.py b/src/inspect_scout/_llm_scanner/__init__.py index d67a2492e..5bd482446 100644 --- a/src/inspect_scout/_llm_scanner/__init__.py +++ b/src/inspect_scout/_llm_scanner/__init__.py @@ -1,4 +1,18 @@ from ._llm_scanner import llm_scanner -from .types import AnswerMultiLabel, AnswerStructured +from ._reducer import ResultReducer +from .answer import Answer, answer_type +from .generate import generate_answer, parse_answer, scanner_prompt +from .types import AnswerMultiLabel, AnswerSpec, AnswerStructured -__all__ = ["llm_scanner", "AnswerMultiLabel", "AnswerStructured"] +__all__ = [ + "Answer", + "AnswerMultiLabel", + "AnswerSpec", + "AnswerStructured", + "ResultReducer", + "answer_type", + "generate_answer", + "llm_scanner", + "parse_answer", + "scanner_prompt", +] diff --git a/src/inspect_scout/_llm_scanner/_llm_scanner.py b/src/inspect_scout/_llm_scanner/_llm_scanner.py index 177cad3fb..5c7f7901d 100644 --- a/src/inspect_scout/_llm_scanner/_llm_scanner.py +++ b/src/inspect_scout/_llm_scanner/_llm_scanner.py @@ -1,35 +1,37 @@ -from typing import Any, Awaitable, Callable, Literal, overload +from typing import Any, Awaitable, Callable, Literal, cast, overload from inspect_ai.model import ( + ChatMessage, Model, - ModelConfig, get_model, ) -from inspect_ai.model._model_config import model_config_to_model, model_to_model_config from inspect_ai.scorer import ValueToFloat from jinja2 import Environment -from inspect_scout._llm_scanner.structured import structured_generate, structured_schema from inspect_scout._util.jinja import StrictOnUseUndefined -from inspect_scout._util.refusal import generate_retry_refusals -from .._scanner.extract import MessagesPreprocessor, messages_as_str -from .._scanner.result import Result -from .._scanner.scanner import SCANNER_NAME_ATTR, Scanner, scanner -from .._transcript.types import Transcript +from .._scanner.extract import MessagesPreprocessor, message_numbering +from .._scanner.result import Result, as_resultset +from .._scanner.scanner import ( + SCANNER_CONTENT_ATTR, + SCANNER_NAME_ATTR, + Scanner, + scanner, +) +from .._transcript.messages import transcript_messages +from .._transcript.types import Transcript, TranscriptContent +from ._reducer import default_reducer, is_resultset_answer from .answer import Answer, answer_from_argument +from .generate import generate_answer from .prompt import DEFAULT_SCANNER_TEMPLATE -from .types import AnswerMultiLabel, AnswerStructured +from .types import AnswerSpec @overload def llm_scanner( *, question: str | Callable[[Transcript], Awaitable[str]], - answer: Literal["boolean", "numeric", "string"] - | list[str] - | AnswerMultiLabel - | AnswerStructured, + answer: AnswerSpec, value_to_float: ValueToFloat | None = None, template: str | None = None, template_variables: dict[str, Any] @@ -37,8 +39,14 @@ def llm_scanner( | None = None, preprocessor: MessagesPreprocessor[Transcript] | None = None, model: str | Model | None = None, + model_role: str | None = None, retry_refusals: bool | int = 3, name: str | None = None, + content: TranscriptContent | None = None, + context_window: int | None = None, + compaction: Literal["all", "last"] | int = "all", + depth: int | None = None, + reducer: Callable[[list[Result]], Awaitable[Result]] | None = None, ) -> Scanner[Transcript]: ... @@ -46,10 +54,7 @@ def llm_scanner( def llm_scanner( *, question: None = None, - answer: Literal["boolean", "numeric", "string"] - | list[str] - | AnswerMultiLabel - | AnswerStructured, + answer: AnswerSpec, value_to_float: ValueToFloat | None = None, template: str, template_variables: dict[str, Any] @@ -57,8 +62,14 @@ def llm_scanner( | None = None, preprocessor: MessagesPreprocessor[Transcript] | None = None, model: str | Model | None = None, + model_role: str | None = None, retry_refusals: bool | int = 3, name: str | None = None, + content: TranscriptContent | None = None, + context_window: int | None = None, + compaction: Literal["all", "last"] | int = "all", + depth: int | None = None, + reducer: Callable[[list[Result]], Awaitable[Result]] | None = None, ) -> Scanner[Transcript]: ... @@ -66,10 +77,7 @@ def llm_scanner( def llm_scanner( *, question: str | Callable[[Transcript], Awaitable[str]] | None = None, - answer: Literal["boolean", "numeric", "string"] - | list[str] - | AnswerMultiLabel - | AnswerStructured, + answer: AnswerSpec, value_to_float: ValueToFloat | None = None, template: str | None = None, template_variables: dict[str, Any] @@ -77,12 +85,25 @@ def llm_scanner( | None = None, preprocessor: MessagesPreprocessor[Transcript] | None = None, model: str | Model | None = None, + model_role: str | None = None, retry_refusals: bool | int = 3, name: str | None = None, + content: TranscriptContent | None = None, + context_window: int | None = None, + compaction: Literal["all", "last"] | int = "all", + depth: int | None = None, + reducer: Callable[[list[Result]], Awaitable[Result]] | None = None, ) -> Scanner[Transcript]: """Create a scanner that uses an LLM to scan transcripts. - This scanner presents a conversation transcript to an LLM along with a custom prompt and answer specification, enabling automated analysis of conversations for specific patterns, behaviors, or outcomes. + This scanner presents a conversation transcript to an LLM along with a + custom prompt and answer specification, enabling automated analysis of + conversations for specific patterns, behaviors, or outcomes. + + Messages are extracted via ``transcript_messages()``, which automatically + selects the best strategy (timelines → events → raw messages) and respects + context window limits. Each segment is scanned independently with globally + unique message numbering (``[M1]``, ``[M2]``, ...) across all segments. Args: question: Question for the scanner to answer. @@ -102,15 +123,43 @@ def llm_scanner( Optionally takes a function which receives the current `Transcript` which can return variables. preprocessor: Transform conversation messages before analysis. - Controls exclusion of system messages, reasoning tokens, and tool calls. Defaults to removing system messages. + Controls exclusion of system messages, reasoning tokens, and tool calls. + Defaults to removing system messages. Note: custom ``transform`` + functions that accept a ``Transcript`` are not supported when + ``context_window`` or timeline scanning produces multiple segments, + as the transform receives ``list[ChatMessage]`` per segment. model: Optional model specification. - Can be a model name string or `Mode`l instance. If None, uses the default model - retry_refusals: Retry model refusals. Pass an `int` for number of retries (defaults to 3). Pass `False` to not retry refusals. If the limit of refusals is exceeded then a `RuntimeError` is raised. + Can be a model name string or ``Model`` instance. If None, uses the default model. + model_role: Optional model role for role-based model resolution. + When set, the model is resolved via ``get_model(model, role=model_role)`` + at scan time, allowing deferred role resolution when roles are not yet + available at scanner construction time. + retry_refusals: Retry model refusals. Pass an ``int`` for number of retries (defaults to 3). Pass ``False`` to not retry refusals. If the limit of refusals is exceeded then a ``RuntimeError`` is raised. name: Scanner name. - Use this to assign a name when passing `llm_scanner()` directly to `scan()` rather than delegating to it from another scanner. + Use this to assign a name when passing ``llm_scanner()`` directly to ``scan()`` rather than delegating to it from another scanner. + content: Override the transcript content filters for this scanner. + For example, ``TranscriptContent(timeline=True)`` requests timeline + data so the scanner can process span-level segments. + context_window: Override the model's context window size for chunking. + When set, transcripts exceeding this limit are split into multiple + segments, each scanned independently. + compaction: How to handle compaction boundaries when extracting + messages from events. ``"all"`` (default) scans all compaction + segments; ``"last"`` scans only the most recent. + depth: Maximum depth of the span tree to process when timelines + are present. ``None`` (default) processes all depths. Ignored + for events-only or messages-only transcripts. + reducer: Custom reducer for aggregating multi-segment results. + Accepts any ``Callable[[list[Result]], Awaitable[Result]]``. + If None, uses a default reducer based on the answer type + (e.g., ``ResultReducer.any`` for boolean, ``ResultReducer.mean`` + for numeric). Standard reducers are available on + :class:`ResultReducer`. Timeline and resultset answers bypass + reduction and return a resultset. Returns: - A `Scanner` function that analyzes Transcript instances and returns `Results` based on the LLM's assessment according to the specified prompt and answer format + A ``Scanner`` function that analyzes Transcript instances and returns + ``Result`` (single segment) or ``list[Result]`` (multiple segments). """ if template is None: template = DEFAULT_SCANNER_TEMPLATE @@ -125,79 +174,66 @@ def llm_scanner( else 0 ) - # Convert Model instances to serializable ModelConfig so the closure - # can survive cloudpickle roundtrips in multiprocess scanning. - serializable_model: str | ModelConfig | None - if isinstance(model, Model): - serializable_model = model_to_model_config(model) - else: - serializable_model = model - async def scan(transcript: Transcript) -> Result: - resolved_model: str | Model | None = ( - model_config_to_model(serializable_model) - if isinstance(serializable_model, ModelConfig) - else serializable_model - ) + # Resolve the model once — defers role resolution to scan time + resolved_model = get_model(model, role=model_role) - messages_str, extract_references = await messages_as_str( - transcript, - preprocessor=preprocessor, - include_ids=True, - ) - - resolved_prompt = await render_scanner_prompt( - template=template, - template_variables=template_variables, - transcript=transcript, - messages=messages_str, - question=question, - answer=resolved_answer, + # Shared numbering scope across all segments + messages_as_str_fn, extract_references = message_numbering( + preprocessor=cast( + MessagesPreprocessor[list[ChatMessage]] | None, preprocessor + ), ) - # do a structured generate if this is AnswerStructured - if isinstance(answer, AnswerStructured): - value, _, model_output = await structured_generate( - input=resolved_prompt, - schema=structured_schema(answer), - answer_tool=answer.answer_tool, - model=resolved_model, - max_attempts=answer.max_attempts, - retry_refusals=retry_refusals, + results: list[Result] = [] + async for segment in transcript_messages( + transcript, + messages_as_str=messages_as_str_fn, + model=resolved_model, + context_window=context_window, + compaction=compaction, + depth=depth, + ): + prompt = await render_scanner_prompt( + template=template, + template_variables=template_variables, + transcript=transcript, + messages=segment.messages_str, + question=question, + answer=resolved_answer, ) - # if we failed to extract then return value=None - if value is None: - return Result( - value=None, - answer=model_output.completion, - metadata={"stop_reason": model_output.stop_reason}, + results.append( + await generate_answer( + prompt, + answer, + model=resolved_model, + retry_refusals=retry_refusals, + extract_refs=extract_references, + value_to_float=value_to_float, ) - - # otherwise do a normal generate - else: - model_output = await generate_retry_refusals( - get_model(resolved_model), - resolved_prompt, - tools=[], - tool_choice=None, - config=None, - retry_refusals=retry_refusals, ) - # resolve answer - result = resolved_answer.result_for_answer( - model_output, extract_references, value_to_float - ) - result.metadata = { - **(result.metadata or {}), - "stop_reason": model_output.stop_reason, - } - return result + # single result + if len(results) == 1: + return results[0] + + # scenarios where resultset is the natural/expected return type + elif bool(transcript.timelines) or is_resultset_answer(answer): + return as_resultset(results) + + # otherwise reduce + else: + effective_reducer = reducer or default_reducer(answer) + return await effective_reducer(results) # set name for collection by @scanner if specified if name is not None: setattr(scan, SCANNER_NAME_ATTR, name) + # set content override for @scanner to merge into ScannerConfig + if content is not None: + setattr(scan, SCANNER_CONTENT_ATTR, content) + return scan diff --git a/src/inspect_scout/_llm_scanner/_reducer.py b/src/inspect_scout/_llm_scanner/_reducer.py new file mode 100644 index 000000000..373f80cf6 --- /dev/null +++ b/src/inspect_scout/_llm_scanner/_reducer.py @@ -0,0 +1,304 @@ +"""Result aggregation for multi-segment scanning. + +Provides :class:`ResultReducer` with standard reducers (mean, any, union, etc.) +and a factory for LLM-based reduction, plus helpers for default reducer +dispatch and resultset detection. +""" + +import builtins +import statistics +from collections.abc import Awaitable, Callable +from typing import get_args, get_origin + +from inspect_ai.model import ChatMessage, ChatMessageSystem, ChatMessageUser, Model +from pydantic import BaseModel, JsonValue + +from .._scanner.result import Reference, Result +from .generate import generate_answer +from .types import AnswerMultiLabel, AnswerSpec, AnswerStructured + + +def _numeric_values(results: list[Result]) -> list[float]: + """Extract numeric values from results, skipping non-numeric.""" + values: list[float] = [] + for r in results: + if isinstance(r.value, int | float) and not isinstance(r.value, bool): + values.append(float(r.value)) + return values + + +def _merge_explanations(results: list[Result]) -> str | None: + """Concatenate non-None explanations with segment markers.""" + parts: list[str] = [] + for i, r in enumerate(results): + if r.explanation is not None: + parts.append(f"[Segment {i + 1}]\n{r.explanation}") + return "\n\n".join(parts) if parts else None + + +def _merge_metadata(results: list[Result]) -> dict[str, object] | None: + """Shallow-merge all metadata dicts (later segments override).""" + merged: dict[str, object] = {} + for r in results: + if r.metadata is not None: + merged.update(r.metadata) + return merged if merged else None + + +def _merge_references(results: list[Result]) -> list[Reference]: + """Combine all references, deduplicated by (type, id).""" + seen: set[tuple[str, str]] = set() + refs: list[Reference] = [] + for r in results: + for ref in r.references: + key = (ref.type, ref.id) + if key not in seen: + seen.add(key) + refs.append(ref) + return refs + + +def _build_result( + results: list[Result], + *, + value: JsonValue, + answer: str | None, +) -> Result: + """Build a reduced Result with merged auxiliary fields.""" + return Result( + value=value, + answer=answer, + explanation=_merge_explanations(results), + metadata=_merge_metadata(results), + references=_merge_references(results), + ) + + +class ResultReducer: + """Standard reducers for aggregating multi-segment scan results. + + Each static method is an async reducer with signature + ``(list[Result]) -> Result``. The :meth:`llm` factory returns a + reducer that uses an LLM to synthesize results. + """ + + @staticmethod + async def mean(results: list[Result]) -> Result: + """Arithmetic mean of numeric values.""" + values = _numeric_values(results) + if not values: + return results[-1] + computed = statistics.mean(values) + return _build_result(results, value=computed, answer=str(computed)) + + @staticmethod + async def median(results: list[Result]) -> Result: + """Median of numeric values.""" + values = _numeric_values(results) + if not values: + return results[-1] + computed = statistics.median(values) + return _build_result(results, value=computed, answer=str(computed)) + + @staticmethod + async def mode(results: list[Result]) -> Result: + """Mode (most common) of numeric values.""" + values = _numeric_values(results) + if not values: + return results[-1] + computed = statistics.mode(values) + return _build_result(results, value=computed, answer=str(computed)) + + @staticmethod + async def max(results: list[Result]) -> Result: + """Maximum of numeric values.""" + values = _numeric_values(results) + if not values: + return results[-1] + computed = builtins.max(values) + return _build_result(results, value=computed, answer=str(computed)) + + @staticmethod + async def min(results: list[Result]) -> Result: + """Minimum of numeric values.""" + values = _numeric_values(results) + if not values: + return results[-1] + computed = builtins.min(values) + return _build_result(results, value=computed, answer=str(computed)) + + @staticmethod + async def any(results: list[Result]) -> Result: + """Boolean OR — True if any result is True.""" + value = builtins.any(r.value is True for r in results) + answer = "Yes" if value else "No" + return _build_result(results, value=value, answer=answer) + + @staticmethod + async def union(results: list[Result]) -> Result: + """Union of list values, deduplicated, preserving order.""" + seen: set[str] = set() + combined: list[str] = [] + for r in results: + if isinstance(r.value, list): + for item in r.value: + s = str(item) + if s not in seen: + seen.add(s) + combined.append(s) + answer = ", ".join(combined) if combined else None + return _build_result(results, value=list(combined), answer=answer) + + @staticmethod + async def majority(results: list[Result]) -> Result: + """Most common value, with last-result tiebreaker. + + Counts occurrences of each ``value`` across results. If there is + a unique winner it is used; otherwise the last result's value + breaks the tie. The ``answer`` is taken from the matching result. + """ + counts: dict[str, int] = {} + for r in results: + key = str(r.value) + counts[key] = counts.get(key, 0) + 1 + + max_count = builtins.max(counts.values()) + winners = [v for v, c in counts.items() if c == max_count] + + if len(winners) == 1: + winning_key = winners[0] + else: + # Tiebreak: pick the last result whose value is among the winners + winning_key = winners[0] # fallback + for r in reversed(results): + if str(r.value) in winners: + winning_key = str(r.value) + break + + # Find the last result whose value matches the winner + matched = results[-1] # fallback + for r in reversed(results): + if str(r.value) == winning_key: + matched = r + break + + return _build_result(results, value=matched.value, answer=matched.answer) + + @staticmethod + async def last(results: list[Result]) -> Result: + """Return the last result with merged auxiliary fields.""" + last_result = results[-1] + return _build_result( + results, + value=last_result.value, + answer=last_result.answer, + ) + + @staticmethod + def llm( + model: str | Model | None = None, + prompt: str | None = None, + ) -> Callable[[list[Result]], Awaitable[Result]]: + """Factory that returns an LLM-based reducer. + + The returned reducer formats all segment results into a prompt + and asks the model to synthesize the best answer. + + Args: + model: Model to use for synthesis. Defaults to the + default model. + prompt: Custom synthesis prompt template. If None, uses + a default template. + + Returns: + An async reducer function. + """ + + async def reducer(results: list[Result]) -> Result: + segments_text = _format_segments_for_llm(results) + synthesis_prompt = prompt or _DEFAULT_SYNTHESIS_PROMPT + + messages: list[ChatMessage] = [ + ChatMessageSystem(content=_SYNTHESIS_SYSTEM_PROMPT), + ChatMessageUser(content=f"{synthesis_prompt}\n\n{segments_text}"), + ] + + result = await generate_answer( + messages, + answer="string", + model=model, + ) + + return _build_result( + results, + value=result.value, + answer=result.answer, + ) + + return reducer + + +_SYNTHESIS_SYSTEM_PROMPT = """\ +You are an expert analyst synthesizing results from a multi-segment transcript analysis. \ +Your task is to combine per-segment findings into a single coherent answer. \ +Be concise and focus on the overall assessment rather than restating each segment.""" + +_DEFAULT_SYNTHESIS_PROMPT = """\ +The following are results from analyzing different segments of a conversation transcript. \ +Each segment was analyzed independently. Please synthesize these into a single best answer. + +Provide your synthesis reasoning, then give your final answer on a line starting with "ANSWER:".""" + + +def _format_segments_for_llm(results: list[Result]) -> str: + """Format segment results for the LLM synthesis prompt.""" + parts: list[str] = [] + for i, r in enumerate(results): + lines = [f"--- Segment {i + 1} ---"] + if r.answer is not None: + lines.append(f"Answer: {r.answer}") + lines.append(f"Value: {r.value}") + if r.explanation is not None: + lines.append(f"Explanation: {r.explanation}") + parts.append("\n".join(lines)) + return "\n\n".join(parts) + + +def is_resultset_answer(answer: AnswerSpec) -> bool: + """Check if the answer spec produces a resultset (list of models).""" + if isinstance(answer, AnswerStructured): + origin = get_origin(answer.type) + if origin is list: + args = get_args(answer.type) + if args and isinstance(args[0], type) and issubclass(args[0], BaseModel): + return True + return False + return False + + +def default_reducer( + answer: AnswerSpec, +) -> Callable[[list[Result]], Awaitable[Result]]: + """Return the default reducer for the given answer spec. + + Args: + answer: The answer specification. + + Returns: + An async reducer function. + """ + match answer: + case "boolean": + return ResultReducer.any + case "numeric": + return ResultReducer.mean + case "string": + return ResultReducer.llm() + case list(): + return ResultReducer.majority + case AnswerMultiLabel(): + return ResultReducer.union + case AnswerStructured(): + return ResultReducer.last + case _: + return ResultReducer.last diff --git a/src/inspect_scout/_llm_scanner/answer.py b/src/inspect_scout/_llm_scanner/answer.py index 9ba442c48..07077ad4d 100644 --- a/src/inspect_scout/_llm_scanner/answer.py +++ b/src/inspect_scout/_llm_scanner/answer.py @@ -1,5 +1,5 @@ import re -from typing import Callable, Literal, Protocol, Sequence +from typing import Callable, Literal, Protocol, Sequence, runtime_checkable from inspect_ai._util.pattern import ANSWER_PATTERN_WORD from inspect_ai._util.text import ( @@ -22,6 +22,7 @@ BOOL_ANSWER_FORMAT, BOOL_ANSWER_PROMPT, LABELS_ANSWER_FORMAT_MULTI, + LABELS_ANSWER_FORMAT_MULTI_NONE_SUFFIX, LABELS_ANSWER_FORMAT_SINGLE, LABELS_ANSWER_PROMPT, NUMBER_ANSWER_FORMAT, @@ -62,6 +63,7 @@ def _strip_markdown_formatting(text: str) -> str: return text +@runtime_checkable class Answer(Protocol): """Protocol for LLM scanner answer types.""" @@ -101,11 +103,35 @@ def answer_from_argument( case list(): return _LabelsAnswer(labels=answer) case AnswerMultiLabel(): - return _LabelsAnswer(labels=answer.labels, multi_classification=True) - case AnswerStructured(): - return _StructuredAnswer(answer) + return _LabelsAnswer( + labels=answer.labels, + multi_classification=True, + allow_none=answer.allow_none, + ) case _: - raise ValueError(f"Invalid answer type: {answer}") + return _StructuredAnswer(answer) + + +def answer_type( + answer: Literal["boolean", "numeric", "string"] + | list[str] + | AnswerMultiLabel + | AnswerStructured, +) -> Answer: + """Resolve an answer specification into an Answer object. + + The returned object exposes ``.prompt`` and ``.format`` properties + containing the prompt text and format instructions for the answer type. + + Args: + answer: Answer specification (``"boolean"``, ``"numeric"``, + ``"string"``, ``list[str]``, ``AnswerMultiLabel``, or + ``AnswerStructured``). + + Returns: + An ``Answer`` with ``.prompt`` and ``.format`` properties. + """ + return answer_from_argument(answer) class _BoolAnswer(Answer): @@ -197,9 +223,15 @@ def result_for_answer( class _LabelsAnswer(Answer): """Answer implementation for multiple choice questions.""" - def __init__(self, labels: list[str], multi_classification: bool = False) -> None: + def __init__( + self, + labels: list[str], + multi_classification: bool = False, + allow_none: bool = False, + ) -> None: self.labels = labels self.multi_classification = multi_classification + self.allow_none = allow_none @property def prompt(self) -> str: @@ -212,6 +244,7 @@ def format(self) -> str: formatted_choices, letters = _answer_options(self.labels) format_template = ( LABELS_ANSWER_FORMAT_MULTI + + (LABELS_ANSWER_FORMAT_MULTI_NONE_SUFFIX if self.allow_none else "") if self.multi_classification else LABELS_ANSWER_FORMAT_SINGLE ) @@ -248,6 +281,15 @@ def result_for_answer( valid_characters = [_answer_character(i) for i in range(len(self.labels))] if self.multi_classification: + # "NONE" means no labels apply (only when allow_none is set) + if answer_text.upper() == "NONE" and self.allow_none: + return Result( + value=[], + answer=answer_text, + explanation=explanation, + references=references, + ) + # Parse comma-separated letters answer_letters = [ letter.strip().upper() for letter in answer_text.split(",") diff --git a/src/inspect_scout/_llm_scanner/generate.py b/src/inspect_scout/_llm_scanner/generate.py new file mode 100644 index 000000000..843f9ba18 --- /dev/null +++ b/src/inspect_scout/_llm_scanner/generate.py @@ -0,0 +1,262 @@ +"""Reusable answer generation and parsing for scanning pipelines. + +Provides :func:`parse_answer` for extracting a :class:`Result` from a +:class:`ModelOutput`, and :func:`generate_answer` for driving LLM +generation with optional parsing — usable independently of +:func:`llm_scanner`. +""" + +from collections.abc import Callable +from typing import Literal, overload + +from inspect_ai.model import ( + ChatMessage, + ChatMessageUser, + Model, + ModelOutput, + get_model, +) +from inspect_ai.scorer import ValueToFloat +from jinja2 import Environment + +from .._scanner.result import Reference, Result +from .._util.jinja import StrictOnUseUndefined +from .._util.refusal import generate_retry_refusals +from .answer import Answer, answer_from_argument +from .prompt import DEFAULT_SCANNER_TEMPLATE +from .structured import structured_generate, structured_schema +from .types import AnswerSpec, AnswerStructured, _TextualAnswerSpec + + +def parse_answer( + output: ModelOutput, + answer: AnswerSpec, + extract_refs: Callable[[str], list[Reference]], + value_to_float: ValueToFloat | None = None, +) -> Result: + """Parse a model output into a Result using the answer specification. + + Delegates to the answer type's parsing logic. This is a pure + function — no LLM call is made. + + Args: + output: The model's response to parse. + answer: Answer specification (``"boolean"``, ``"numeric"``, + ``"string"``, ``list[str]``, or :class:`AnswerStructured`). + extract_refs: Function to extract ``[M1]``-style references + from the explanation text. + value_to_float: Optional function to convert the parsed value + to a float. + + Returns: + A Result with value, answer, explanation, and references. + """ + resolved = answer_from_argument(answer) + return resolved.result_for_answer(output, extract_refs, value_to_float) + + +@overload +async def generate_answer( + prompt: str | list[ChatMessage], + answer: AnswerSpec, + *, + model: str | Model | None = None, + retry_refusals: int = 3, + parse: Literal[True] = True, + extract_refs: Callable[[str], list[Reference]] | None = None, + value_to_float: ValueToFloat | None = None, +) -> Result: ... + + +@overload +async def generate_answer( + prompt: str | list[ChatMessage], + answer: _TextualAnswerSpec, + *, + model: str | Model | None = None, + retry_refusals: int = 3, + parse: Literal[False], +) -> ModelOutput: ... + + +async def generate_answer( + prompt: str | list[ChatMessage], + answer: AnswerSpec, + *, + model: str | Model | None = None, + retry_refusals: int = 3, + parse: bool = True, + extract_refs: Callable[[str], list[Reference]] | None = None, + value_to_float: ValueToFloat | None = None, +) -> Result | ModelOutput: + """Generate a model response, optionally parsing it into a Result. + + Dispatches to the appropriate generation strategy based on the answer + type: tool-based structured generation for :class:`AnswerStructured`, + standard text generation with refusal retry for all other types. + + Args: + prompt: The scanning prompt (string or message list). + answer: Answer specification (``"boolean"``, ``"numeric"``, + ``"string"``, ``list[str]``, or :class:`AnswerStructured`). + Determines both the generation strategy and (when parsing) + how to extract the result. + model: Model to use for generation. Can be a model name string + or ``Model`` instance. If ``None``, uses the default model. + retry_refusals: Number of times to retry on model refusals + (``stop_reason == "content_filter"``). + parse: When ``True`` (default), parse the model output into a + :class:`Result`. When ``False``, return the raw + :class:`ModelOutput`. + extract_refs: Function to extract ``[M1]``-style references + from the explanation text. Only used when ``parse=True``. + Defaults to a no-op that returns no references. + value_to_float: Optional function to convert the parsed value + to a float. Only used when ``parse=True``. + + Returns: + A :class:`Result` when ``parse=True``, or a :class:`ModelOutput` + when ``parse=False``. + """ + resolved_answer = answer_from_argument(answer) + + if isinstance(answer, AnswerStructured): + value, _, model_output = await structured_generate( + input=prompt, + schema=structured_schema(answer), + answer_tool=answer.answer_tool, + model=model, + max_attempts=answer.max_attempts, + retry_refusals=retry_refusals, + ) + if value is None: + return Result( + value=None, + answer=model_output.completion, + metadata={"stop_reason": model_output.stop_reason}, + ) + refs_fn = extract_refs or _no_references + result = resolved_answer.result_for_answer( + model_output, refs_fn, value_to_float + ) + result.metadata = { + **(result.metadata or {}), + "stop_reason": model_output.stop_reason, + } + return result + + elif parse: + return await _text_generate( + get_model(model), + prompt, + resolved_answer, + retry_refusals, + extract_refs or _no_references, + value_to_float, + ) + else: + return await generate_retry_refusals( + get_model(model), + prompt, + tools=[], + tool_choice=None, + config=None, + retry_refusals=retry_refusals, + ) + + +_TEXT_MAX_ATTEMPTS = 3 + + +async def _text_generate( + model: Model, + input: str | list[ChatMessage], + answer: Answer, + retry_refusals: int, + extract_refs: Callable[[str], list[Reference]], + value_to_float: ValueToFloat | None, +) -> Result: + """Generate text with retry on parse failure. + + When the model's response cannot be parsed, feeds format instructions + back as a user message and retries, up to ``_TEXT_MAX_ATTEMPTS`` times. + Returns a fully parsed ``Result`` (with refs, value_to_float, and + stop_reason metadata). + """ + messages: list[ChatMessage] = ( + [ChatMessageUser(content=input)] if isinstance(input, str) else list(input) + ) + + for attempt in range(_TEXT_MAX_ATTEMPTS): + output = await generate_retry_refusals( + model, + messages, + tools=[], + tool_choice=None, + config=None, + retry_refusals=retry_refusals, + ) + + result = answer.result_for_answer(output, extract_refs, value_to_float) + result.metadata = { + **(result.metadata or {}), + "stop_reason": output.stop_reason, + } + + if result.answer is not None or attempt == _TEXT_MAX_ATTEMPTS - 1: + return result + + # Parse failed — grow conversation with feedback + messages.append(output.message) + messages.append( + ChatMessageUser( + content=( + "Your response could not be parsed. " + "Please try again, following these instructions:\n\n" + f"{answer.format}" + ), + ) + ) + + raise AssertionError("unreachable") # loop always returns + + +def _no_references(_text: str) -> list[Reference]: + return [] + + +def scanner_prompt( + messages: str, + question: str, + answer: AnswerSpec | Answer, +) -> str: + """Render the default scanner prompt template. + + Combines the transcript messages, question, and answer type into a + complete prompt using the standard scanner template. Use this when + you want the default prompt structure but need control over generation + (via ``generate_answer()``) or other scanning steps. + + For fully custom prompt templates, use ``answer_type()`` to access + the ``.prompt`` and ``.format`` strings directly. + + Args: + messages: Pre-formatted message string (from ``messages_as_str`` + or ``message_numbering``). + question: Question for the scanner to answer. + answer: Answer specification or resolved ``Answer`` object. + + Returns: + Rendered prompt string. + """ + resolved = answer if isinstance(answer, Answer) else answer_from_argument(answer) + return ( + Environment(undefined=StrictOnUseUndefined) + .from_string(DEFAULT_SCANNER_TEMPLATE) + .render( + messages=messages, + question=question, + answer_prompt=resolved.prompt, + answer_format=resolved.format, + ) + ) diff --git a/src/inspect_scout/_llm_scanner/prompt.py b/src/inspect_scout/_llm_scanner/prompt.py index a8db6c984..b59ae1628 100644 --- a/src/inspect_scout/_llm_scanner/prompt.py +++ b/src/inspect_scout/_llm_scanner/prompt.py @@ -49,6 +49,10 @@ + "'ANSWER: $LETTERS' (without quotes) where $LETTERS is a comma-separated list of letters from {{ letters }} representing:\n{{ formatted_choices }}" ) +LABELS_ANSWER_FORMAT_MULTI_NONE_SUFFIX = ( + "\n\nOr if none of the options apply, 'ANSWER: NONE'" +) + STR_ANSWER_PROMPT = "Answer the following question about the transcript above:" STR_ANSWER_FORMAT = ( ANSWER_FORMAT_PREAMBLE diff --git a/src/inspect_scout/_llm_scanner/types.py b/src/inspect_scout/_llm_scanner/types.py index 3055d10ad..50650d17d 100644 --- a/src/inspect_scout/_llm_scanner/types.py +++ b/src/inspect_scout/_llm_scanner/types.py @@ -1,4 +1,4 @@ -from typing import NamedTuple +from typing import Literal, NamedTuple, TypeAlias from pydantic import BaseModel @@ -12,6 +12,9 @@ class AnswerMultiLabel(NamedTuple): Label values (e.g. A, B, C) will be provided automatically. """ + allow_none: bool = False + """Allow the model to respond with NONE when no labels apply.""" + class AnswerStructured(NamedTuple): """Answer with structured output. @@ -44,3 +47,17 @@ class AnswerStructured(NamedTuple): max_attempts: int = 3 """Maximum number of times to re-prompt the model to generate the correct schema.""" + + +# We construct AnswerSpec this way so that we can prohibit AnswerStructured w/parse: False +_TextualAnswerSpec: TypeAlias = ( + Literal["boolean", "numeric", "string"] | list[str] | AnswerMultiLabel +) +AnswerSpec: TypeAlias = _TextualAnswerSpec | AnswerStructured +"""Specification of the answer format for an LLM scanner. + +Pass ``"boolean"``, ``"numeric"``, or ``"string"`` for a simple answer; +pass ``list[str]`` for a set of labels; pass :class:`AnswerMultiLabel` +for multi-classification; or pass :class:`AnswerStructured` for +tool-based structured output. +""" diff --git a/src/inspect_scout/_recorder/buffer.py b/src/inspect_scout/_recorder/buffer.py index 9637b47de..2758af37e 100644 --- a/src/inspect_scout/_recorder/buffer.py +++ b/src/inspect_scout/_recorder/buffer.py @@ -50,14 +50,22 @@ def buffer_dir(scan_location: str) -> UPath: normalized = normalize_for_hashing(scan_location) return scan_buffer_dir / f"{mm3_hash(normalized)}" - def __init__(self, scan_location: str, spec: ScanSpec): + def __init__( + self, + scan_location: str, + spec: ScanSpec, + *, + pool_dedup: bool = True, + reset: bool = False, + ): self._buffer_dir = RecorderBuffer.buffer_dir(scan_location) self._buffer_dir.mkdir(parents=True, exist_ok=True) self._spec = spec + self._pool_dedup = pool_dedup - # establish scan summary if required + # establish scan summary scan_summary_file = self._buffer_dir.joinpath(SCAN_SUMMARY) - if not scan_summary_file.exists(): + if reset or not scan_summary_file.exists(): self._scan_summary = Summary( complete=False, scanners=list(spec.scanners.keys()) ) @@ -66,10 +74,11 @@ def __init__(self, scan_location: str, spec: ScanSpec): else: self._scan_summary = read_scan_summary(self._buffer_dir, spec) - # truncate errors + # truncate errors on reset, preserve on resume self._error_file = self._buffer_dir.joinpath(SCAN_ERRORS) - with self._error_file.open("w"): - pass # truncates existing file + if reset or not self._error_file.exists(): + with self._error_file.open("w"): + pass async def record( self, @@ -158,7 +167,7 @@ async def record( "scanner_params": self._spec.scanners[scanner].params, }, ) - | result.to_df_columns() + | result.to_df_columns(pool_dedup=self._pool_dedup) | {"timestamp": datetime.now().astimezone().isoformat()} for result in results ] @@ -442,7 +451,26 @@ def _records_to_arrow(records: list[dict[str, Any]]) -> "pa.Table": if col in record and record[col] is not None: record[col] = str(record[col]) - return pa.Table.from_pylist(norm) + # Build arrays column-wise so string columns can use large_string offsets. + # This avoids the ~2GB limit of Arrow's default string offset type. + columns: list[str] = [] + seen_columns: set[str] = set() + for record in norm: + for key in record: + if key not in seen_columns: + seen_columns.add(key) + columns.append(key) + + arrays: dict[str, pa.Array[Any]] = {} + for column in columns: + values = [record.get(column) for record in norm] + non_null_values = [value for value in values if value is not None] + if non_null_values and all(isinstance(value, str) for value in non_null_values): + arrays[column] = pa.array(values, type=pa.large_string()) + else: + arrays[column] = pa.array(values) + + return pa.table(arrays) def read_scan_errors(error_file: str) -> list[Error]: diff --git a/src/inspect_scout/_recorder/file.py b/src/inspect_scout/_recorder/file.py index a699bd66a..b8446e20c 100644 --- a/src/inspect_scout/_recorder/file.py +++ b/src/inspect_scout/_recorder/file.py @@ -104,7 +104,9 @@ async def init(self, spec: ScanSpec, scans_location: str) -> None: self._write_scan_spec() # create the scan buffer - self._scan_buffer = RecorderBuffer(self._scan_dir.as_posix(), self._scan_spec) + self._scan_buffer = RecorderBuffer( + self._scan_dir.as_posix(), self._scan_spec, reset=True + ) async def snapshot_transcripts(self, snapshot: ScanTranscripts) -> None: assert self._scan_spec @@ -276,22 +278,23 @@ def reader( ), ) - def get_field( - self, scanner: str, id_column: str, id_value: Any, target_column: str - ) -> "Scalar[Any]": + def _get_dataset(self, scanner: str) -> "ds.Dataset": scan_path = UPath(scan_location) scanner_path = scan_path / f"{scanner}.parquet" scanner_path_str = scanner_path.as_posix() # For remote filesystems, pre-fetch the file into memory - dataset: ds.Dataset if scanner_path_str.startswith(("s3://", "gs://", "az://", "abfs://")): with file(scanner_path_str, "rb") as f: file_bytes = f.read() table = pq.read_table(io.BytesIO(file_bytes)) - dataset = ds.dataset(table) - else: - dataset = ds.dataset(str(scanner_path), format="parquet") + return ds.dataset(table) + return ds.dataset(str(scanner_path), format="parquet") + + def get_field( + self, scanner: str, id_column: str, id_value: Any, target_column: str + ) -> "Scalar[Any]": + dataset = self._get_dataset(scanner) table = dataset.to_table( columns=[target_column], @@ -308,6 +311,40 @@ def get_field( return cast("Scalar[Any]", table[target_column][0]) + def get_fields( + self, + scanner: str, + id_column: str, + id_value: Any, + target_columns: list[str], + ) -> dict[str, Any]: + """Fetch multiple columns for a single row in one parquet scan. + + Missing columns (e.g. in old parquet files) return None. + """ + dataset = self._get_dataset(scanner) + schema_names = set(dataset.schema.names) + present = [c for c in target_columns if c in schema_names] + missing = [c for c in target_columns if c not in schema_names] + + table = dataset.to_table( + columns=present, + filter=(pc.field(id_column) == id_value), + ) + + if len(table) == 0: + raise KeyError(f"{id_value!r} not found in {id_column}") + + if len(table) > 1: + raise ValueError( + f"Multiple rows found for {id_column}={id_value!r}" + ) + + result = {c: table[c][0].as_py() for c in present} + for c in missing: + result[c] = None + return result + # get the status status = await FileRecorder.status(scan_location) @@ -498,7 +535,7 @@ def _create_expanded_view_sql( 1. Passes through non-resultset rows as-is 2. Unnests resultset rows using json_extract and UNNEST 3. Extracts Result fields into columns - 4. Applies type casting to the value column + 4. Extracts value as string (typed conversion handled downstream) Args: conn: DuckDB connection to query schema @@ -558,19 +595,10 @@ def _create_expanded_view_sql( if col not in result_fields and col not in scan_execution_fields ] - # Type casting expression for the extracted value - # elem will be a JSON string from UNNEST, need to cast it first - cast_value_expr = """CASE - WHEN COALESCE(json_extract_string(CAST(elem AS JSON), '$.type'), 'null') = 'boolean' - THEN CASE - WHEN json_extract_string(CAST(elem AS JSON), '$.value') IN ('true', 'True') THEN TRUE - WHEN json_extract_string(CAST(elem AS JSON), '$.value') IN ('false', 'False') THEN FALSE - ELSE NULL - END - WHEN COALESCE(json_extract_string(CAST(elem AS JSON), '$.type'), 'null') = 'number' - THEN TRY_CAST(json_extract_string(CAST(elem AS JSON), '$.value') AS DOUBLE) - ELSE json_extract(CAST(elem AS JSON), '$.value') - END""" + # Extract value as string — downstream _cast_value_column() (Python) and + # castValue() (TypeScript) handle typed conversion. Kept as string to avoid + # DuckDB 1.5 mixed-type CASE expression errors. + cast_value_expr = "json_extract_string(CAST(elem AS JSON), '$.value')" # Build column selects for expanded query in the same order as all_columns expanded_col_selects = [] diff --git a/src/inspect_scout/_recorder/recorder.py b/src/inspect_scout/_recorder/recorder.py index 6efb754c3..051e9f3c9 100644 --- a/src/inspect_scout/_recorder/recorder.py +++ b/src/inspect_scout/_recorder/recorder.py @@ -73,6 +73,15 @@ def get_field( self, scanner: str, id_column: str, id_value: Any, target_column: str ) -> "Scalar[Any]": ... + @abc.abstractmethod + def get_fields( + self, + scanner: str, + id_column: str, + id_value: Any, + target_columns: list[str], + ) -> dict[str, Any]: ... + @dataclass class ScanResultsDF(Status): diff --git a/src/inspect_scout/_recorder/summary.py b/src/inspect_scout/_recorder/summary.py index c97b0e756..2ae5ed01b 100644 --- a/src/inspect_scout/_recorder/summary.py +++ b/src/inspect_scout/_recorder/summary.py @@ -9,6 +9,7 @@ ) from inspect_scout._scanner.result import ResultReport from inspect_scout._transcript.types import TranscriptInfo +from inspect_scout._validation.validate import is_positive_value class ScannerSummary(BaseModel): @@ -104,7 +105,12 @@ def _report( if result.result.type == "resultset" and isinstance( result.result.value, list ): - agg_results += len(result.result.value) + agg_results += sum( + 1 + for item in result.result.value + if isinstance(item, dict) + and is_positive_value(item.get("value")) + ) else: agg_results += 1 if result.validation is not None: diff --git a/src/inspect_scout/_scanner/_loaders.py b/src/inspect_scout/_scanner/_loaders.py index 99d072357..df8dbb494 100644 --- a/src/inspect_scout/_scanner/_loaders.py +++ b/src/inspect_scout/_scanner/_loaders.py @@ -9,12 +9,13 @@ get_origin, ) +from inspect_ai.event import Timeline from inspect_ai.event._event import Event from inspect_ai.model._chat_message import ChatMessage from typing_extensions import Literal from .._transcript.types import Transcript, TranscriptContent -from .._transcript.util import filter_list, filter_transcript +from .._transcript.util import filter_list, filter_timelines, filter_transcript from .loader import Loader, loader from .types import ScannerInput @@ -68,6 +69,38 @@ async def the_loader( return the_factory() +def _TimelineListLoader( + content: TranscriptContent, +) -> Loader[ScannerInput]: + @loader(name="TimelineListLoader", content=content) + def the_factory() -> Loader[ScannerInput]: + async def the_loader( + transcript: Transcript, + ) -> AsyncIterator[list[Timeline]]: + yield filter_timelines(transcript.timelines, content.timeline) + + return the_loader + + return the_factory() + + +def _TimelineItemLoader( + content: TranscriptContent, +) -> Loader[ScannerInput]: + @loader(name="TimelineItemLoader", content=content) + def the_factory() -> Loader[ScannerInput]: + async def the_loader( + transcript: Transcript, + ) -> AsyncIterator[Timeline]: + filtered = filter_timelines(transcript.timelines, content.timeline) + if filtered: + yield filtered[0] + + return the_loader + + return the_factory() + + def create_implicit_loader( scanner_fn: Callable[..., Any], content: TranscriptContent ) -> Loader[ScannerInput]: @@ -94,9 +127,16 @@ def create_implicit_loader( args = get_args(input_annotation) assert args, "Scanner input list type annotation must not be bare" element_type = args[0] + # Check for list[Timeline] + if element_type is Timeline: + return _TimelineListLoader(content) # Return list loader (yields entire list) return _ListLoader(_message_or_event(element_type), content) + # Check for single Timeline + if input_annotation is Timeline: + return _TimelineItemLoader(content) + # Otherwise it's a single item type (ChatMessage or Event) # Return item loader (yields individual items) return _ListItemLoader(_message_or_event(input_annotation), content) diff --git a/src/inspect_scout/_scanner/extract.py b/src/inspect_scout/_scanner/extract.py index d183d4b10..eee6cb010 100644 --- a/src/inspect_scout/_scanner/extract.py +++ b/src/inspect_scout/_scanner/extract.py @@ -2,7 +2,7 @@ import re from dataclasses import dataclass from functools import reduce -from typing import Awaitable, Callable, Generic, Literal, TypeVar, overload +from typing import Awaitable, Callable, Generic, Literal, TypeAlias, TypeVar, overload from inspect_ai.model import ( ChatMessage, @@ -136,6 +136,56 @@ def reduce_message( ) +MessagesAsStr: TypeAlias = Callable[[list[ChatMessage]], Awaitable[str]] +ExtractReferences: TypeAlias = Callable[[str], list[Reference]] + + +def message_numbering( + preprocessor: MessagesPreprocessor[list[ChatMessage]] | None = None, +) -> tuple[MessagesAsStr, ExtractReferences]: + """Create a messages_as_str / extract_references pair with shared numbering. + + Returns two functions that share a closure-captured counter and id_map. + Each call to the returned messages_as_str auto-increments message IDs + globally, so multiple calls produce M1..M5, then M6..M10, etc. + + The returned extract_references resolves citations from ANY prior + messages_as_str call within this numbering scope. + + Args: + preprocessor: Message preprocessing options applied to every call + (e.g., exclude_system, exclude_reasoning). Defaults to excluding + system messages when no preprocessor is provided. + + Returns: + Tuple of: + - messages_as_str: takes list[ChatMessage], returns formatted string with globally unique [M1], [M2], etc. prefixes. + - extract_refs: takes text, returns list of Reference objects for any [M1], [M2], etc. references found across all prior calls. + """ + counter = [0] + id_map: dict[str, str] = {} + + async def _messages_as_str(messages: list[ChatMessage]) -> str: + if preprocessor is not None and preprocessor.transform is not None: + messages = await preprocessor.transform(messages) + + items: list[str] = [] + for message in messages: + content = message_as_str(message, preprocessor) + if content is not None: + counter[0] += 1 + ordinal = f"M{counter[0]}" + id_map[ordinal] = _message_id(message) + items.append(f"[{ordinal}] {content}") + + return "\n".join(items) + + def _extract_refs(text: str) -> list[Reference]: + return _extract_references(text, id_map) + + return _messages_as_str, _extract_refs + + def message_as_str( message: ChatMessage, preprocessor: MessageFormatOptions | None = None, @@ -246,14 +296,14 @@ def _extract_references(text: str, id_map: dict[str, str]) -> list[Reference]: """Extract message and event references from text. Args: - text: Text containing [M{n}] or [E{n}] style references + text: Text containing [M{n}], [E{n}], M{n}, or E{n} style references id_map: Dict mapping ordinal IDs (e.g., "M1", "M2", "E1", "E2") to actual IDs Returns: List of Reference objects with type="message" or type="event" """ - # Find all [M{number}] or [E{number}] patterns in the text - pattern = r"\[(M|E)\d+\]" + # Match bracketed [M1]/[E1] or bare M1/E1 with word boundaries + pattern = r"\[(M|E)\d+\]|\b(M|E)\d+\b" matches = re.finditer(pattern, text) references = [] @@ -261,8 +311,8 @@ def _extract_references(text: str, id_map: dict[str, str]) -> list[Reference]: for match in matches: cite = match.group(0) - # Extract ordinal key (e.g., "M1" from "[M1]" or "E1" from "[E1]") - ordinal_key = cite[1:-1] + # Extract ordinal key: "M1" from "[M1]" or "M1" from bare "M1" + ordinal_key = cite[1:-1] if cite.startswith("[") else cite # Look up actual ID if ordinal_key in id_map: diff --git a/src/inspect_scout/_scanner/filter.py b/src/inspect_scout/_scanner/filter.py index 0a7b9d53f..8f1f3119c 100644 --- a/src/inspect_scout/_scanner/filter.py +++ b/src/inspect_scout/_scanner/filter.py @@ -46,6 +46,34 @@ def validate_messages_filter(filter: list[MessageType] | None) -> None: ) +TIMELINE_DEFAULT_EVENTS: list[EventType] = [ + "model", + "tool", + "approval", + "compaction", + "error", + "span_begin", + "span_end", +] + + +def normalize_timeline_filter( + filter: Literal[True] | list[EventType] | Literal["all"], +) -> list[EventType] | Literal["all"]: + if filter is True: + return list(TIMELINE_DEFAULT_EVENTS) + if filter == "all": + return filter + uniq: list[EventType] = [] + seen: set[EventType] = set() + for x in filter: + if x not in seen: + uniq.append(x) + seen.add(x) + validate_events_filter(uniq) + return uniq + + def validate_events_filter(filter: list[EventType] | None) -> None: if filter is None: return diff --git a/src/inspect_scout/_scanner/loader.py b/src/inspect_scout/_scanner/loader.py index 769d4c2cc..830eef21d 100644 --- a/src/inspect_scout/_scanner/loader.py +++ b/src/inspect_scout/_scanner/loader.py @@ -29,6 +29,7 @@ from .filter import ( normalize_events_filter, normalize_messages_filter, + normalize_timeline_filter, ) from .types import ScannerInput @@ -152,6 +153,7 @@ def loader( name: str | None = None, messages: list[MessageType] | Literal["all"] | None = None, events: list[EventType] | Literal["all"] | None = None, + timeline: Literal[True] | list[EventType] | Literal["all"] | None = None, content: TranscriptContent | None = None, ) -> Callable[[LoaderFactory[P, TLoaderResult]], LoaderFactory[P, TLoaderResult]]: """Decorator for registering loaders. @@ -160,21 +162,27 @@ def loader( name: Loader name (defaults to function name). messages: Message types to load from. events: Event types to load from. + timeline: Event types to include in timelines. content: Transcript content filter. Returns: Loader with registry info. """ if content is None: - if messages is None and events is None: - raise RuntimeError("Must filter on messages or events") + if messages is None and events is None and timeline is None: + raise RuntimeError("Must filter on messages, events, or timeline") content = TranscriptContent( - normalize_messages_filter(messages) if messages is not None else None, - normalize_events_filter(events) if events is not None else None, + messages=( + normalize_messages_filter(messages) if messages is not None else None + ), + events=normalize_events_filter(events) if events is not None else None, + timeline=( + normalize_timeline_filter(timeline) if timeline is not None else None + ), ) else: - assert messages is None and events is None, ( - "Don't pass messages or events if you pass content" + assert messages is None and events is None and timeline is None, ( + "Don't pass messages, events, or timeline if you pass content" ) def decorate( diff --git a/src/inspect_scout/_scanner/result.py b/src/inspect_scout/_scanner/result.py index ef47b77ee..31c589a54 100644 --- a/src/inspect_scout/_scanner/result.py +++ b/src/inspect_scout/_scanner/result.py @@ -1,13 +1,16 @@ import json from logging import getLogger -from typing import Any, Literal, Sequence +from typing import Any, Literal, Sequence, cast from inspect_ai._util.json import jsonable_python, to_json_str_safe +from inspect_ai.event._event import Event +from inspect_ai.log import condense_events from inspect_ai.model import ModelUsage from pydantic import BaseModel, ConfigDict, Field, JsonValue from shortuuid import uuid from inspect_scout._scanner.types import ScannerInput, ScannerInputNames +from inspect_scout._transcript.types import Transcript logger = getLogger(__name__) @@ -107,13 +110,17 @@ class ResultReport(BaseModel): model_usage: dict[str, ModelUsage] - def to_df_columns(self) -> dict[str, str | bool | int | float | None]: + def to_df_columns( + self, *, pool_dedup: bool = True + ) -> dict[str, str | bool | int | float | None]: columns: dict[str, str | bool | int | float | None] = {} # input (transcript, event, or message) columns["input_type"] = self.input_type columns["input_ids"] = json.dumps(self.input_ids) - columns["input"] = to_json_str_safe(self.input) + columns["input"], columns["input_data"] = _serialize_input( + self.input, self.input_type, pool_dedup=pool_dedup + ) if self.result is not None: # result @@ -200,3 +207,30 @@ def references_json(type: str) -> str: columns["scan_events"] = to_json_str_safe(self.events) return columns + + +def _serialize_input( + input: ScannerInput, + input_type: ScannerInputNames, + *, + pool_dedup: bool, +) -> tuple[str, str | None]: + """Serialize scanner input, optionally condensing events. + + Returns: + (input_json, input_data_json | None) + """ + if not pool_dedup or input_type not in ("transcript", "events"): + return to_json_str_safe(input), None + + if input_type == "transcript": + assert isinstance(input, Transcript) + condensed_events, events_data = condense_events(input.events) + condensed = input.model_copy(update={"events": condensed_events}) + return to_json_str_safe(condensed), to_json_str_safe(events_data) + + # input_type == "events" + assert isinstance(input, Sequence) + events = cast(Sequence[Event], input) + condensed_events, events_data = condense_events(events) + return to_json_str_safe(condensed_events), to_json_str_safe(events_data) diff --git a/src/inspect_scout/_scanner/scanner.py b/src/inspect_scout/_scanner/scanner.py index 2e509bbeb..4e3cf38b1 100644 --- a/src/inspect_scout/_scanner/scanner.py +++ b/src/inspect_scout/_scanner/scanner.py @@ -48,6 +48,7 @@ from .filter import ( normalize_events_filter, normalize_messages_filter, + normalize_timeline_filter, ) from .loader import Loader from .result import Result @@ -60,6 +61,7 @@ SCANNER_FILE_ATTR = "___scanner_file___" SCANNER_NAME_ATTR = "___scanner_name___" +SCANNER_CONTENT_ATTR = "___scanner_content___" # core types # Use bounded TypeVar (contravariant for scanner input) @@ -216,6 +218,7 @@ def scanner( loader: Loader[TScan] | None = None, messages: list[MessageType] | Literal["all"] | None = None, events: list[EventType] | Literal["all"] | None = None, + timeline: Literal[True] | list[EventType] | Literal["all"] | None = None, name: str | None = None, version: int = 0, metrics: Sequence[Metric | Mapping[str, Sequence[Metric]]] @@ -230,6 +233,7 @@ def scanner( loader: Loader[TScan] | None = None, messages: list[MessageType] | Literal["all"] | None = None, events: list[EventType] | Literal["all"] | None = None, + timeline: Literal[True] | list[EventType] | Literal["all"] | None = None, name: str | None = None, version: int = 0, metrics: Sequence[Metric | Mapping[str, Sequence[Metric]]] @@ -249,6 +253,7 @@ def scanner( loader: Custom data loader for scanner. messages: Message types to scan. events: Event types to scan. + timeline: Event types to include in timelines. name: Scanner name (defaults to function name). version: Scanner version (defaults to 0). metrics: One or more metrics to calculate over the values @@ -265,6 +270,23 @@ def scanner( # Don't raise error here anymore - we'll check after attempting inference messages = normalize_messages_filter(messages) if messages is not None else None events = normalize_events_filter(events) if events is not None else None + timeline = normalize_timeline_filter(timeline) if timeline is not None else None + + # Timeline implies events (needed for UUID resolution and timeline_build fallback) + if timeline is not None: + if events is None: + if timeline == "all": + events = "all" + else: + # Timeline event types + structural events needed for tree building + events = normalize_events_filter( + list(set(timeline) | {"span_begin", "span_end"}) + ) + elif events != "all": + # Ensure span events are included (needed for tree building) + events = normalize_events_filter( + list(set(events) | {"span_begin", "span_end"}) + ) def decorate(factory_fn: ScannerFactory[P, T]) -> ScannerFactory[P, T]: @wraps(factory_fn) @@ -290,10 +312,16 @@ def factory_wrapper(*args: P.args, **kwargs: P.kwargs) -> Scanner[T]: # Use explicit filters if provided, otherwise try to infer inferred_messages = messages inferred_events = events + inferred_timeline = timeline # Only infer if no loader and no explicit filters - if loader is None and messages is None and events is None: - temp_messages, temp_events = infer_filters_from_type( + if ( + loader is None + and messages is None + and events is None + and timeline is None + ): + temp_messages, temp_events, temp_timeline = infer_filters_from_type( scanner_fn, factory_fn.__globals__ ) # Cast to proper types (mypy can't infer the string literals) @@ -305,21 +333,48 @@ def factory_wrapper(*args: P.args, **kwargs: P.kwargs) -> Scanner[T]: inferred_events = ( cast(list[EventType] | None, temp_events) if temp_events else None ) + if temp_timeline: + inferred_timeline = "all" + # Timeline implies events="all" + if inferred_events is None: + inferred_events = "all" + # If we couldn't infer anything, raise an error - if inferred_messages is None and inferred_events is None: + if ( + inferred_messages is None + and inferred_events is None + and inferred_timeline is None + ): raise ValueError( f"scanner '{scanner_name}' requires at least one of: " - "messages=..., events=..., loader=..., or specific type annotations" + "messages=..., events=..., timeline=..., loader=..., " + "or specific type annotations" ) + # Allow scan functions to override content filters at runtime + # (e.g. llm_scanner sets this when content= is passed) + if hasattr(scanner_fn, SCANNER_CONTENT_ATTR): + override = getattr(scanner_fn, SCANNER_CONTENT_ATTR) + if override.messages is not None: + inferred_messages = override.messages + if override.events is not None: + inferred_events = override.events + if override.timeline is not None: + inferred_timeline = override.timeline + # Validate scanner signature matches filters # Only validate if we have filters (not just a custom loader) - if inferred_messages is not None or inferred_events is not None: + if ( + inferred_messages is not None + or inferred_events is not None + or inferred_timeline is not None + ): validate_scanner_signature( scanner_fn, inferred_messages, inferred_events, factory_fn.__globals__, + timeline=inferred_timeline, ) # compute scanner config @@ -328,6 +383,8 @@ def factory_wrapper(*args: P.args, **kwargs: P.kwargs) -> Scanner[T]: scanner_config.content.messages = inferred_messages if inferred_events is not None: scanner_config.content.events = inferred_events + if inferred_timeline is not None: + scanner_config.content.timeline = inferred_timeline if loader is not None: # TODO: how are we ensuring that the writer of a custom loader sets # the proper content filter? We could do it for them, but I'm not diff --git a/src/inspect_scout/_scanner/scorer.py b/src/inspect_scout/_scanner/scorer.py index 5eb936546..8338c4354 100644 --- a/src/inspect_scout/_scanner/scorer.py +++ b/src/inspect_scout/_scanner/scorer.py @@ -1,8 +1,9 @@ +from copy import deepcopy from typing import Any, Mapping, Sequence, cast from inspect_ai._util.json import to_json_str_safe from inspect_ai._util.registry import is_registry_object, registry_unqualified_name -from inspect_ai.event import Event +from inspect_ai.event import Event, Timeline from inspect_ai.log import transcript as sample_transcript from inspect_ai.model import ChatMessage from inspect_ai.scorer import ( @@ -54,8 +55,11 @@ def scanner_to_scorer() -> Scorer: async def score(state: TaskState, target: Target) -> Score | None: # filter transcript messages and events config = config_for_scanner(scanner) - messages, events = _scanner_messages_and_events( - config, state.messages, list(sample_transcript().events) + messages, events, timelines = _scanner_content( + config, + state.messages, + list(sample_transcript().events), + list(sample_transcript().timelines), ) # prepare transcript from state @@ -66,9 +70,12 @@ async def score(state: TaskState, target: Target) -> Score | None: "id": state.sample_id, "epoch": state.epoch, "sample_metadata": state.metadata, + "target": target.target if len(target) > 1 else target.text, + "scores": state.scores, }, messages=messages, events=events, + timelines=timelines, ) # call scanner @@ -135,9 +142,12 @@ def references_for_type(type: str) -> list[str]: return None -def _scanner_messages_and_events( - config: ScannerConfig, messages: list[ChatMessage], events: list[Event] -) -> tuple[list[ChatMessage], list[Event]]: +def _scanner_content( + config: ScannerConfig, + messages: list[ChatMessage], + events: list[Event], + timelines: list[Timeline], +) -> tuple[list[ChatMessage], list[Event], list[Timeline]]: if config.content.messages == "all": scanner_messages = list(messages) elif isinstance(config.content.messages, list): @@ -149,6 +159,25 @@ def _scanner_messages_and_events( f"Unexpected type for messages: {type(config.content.messages)}" ) + if config.content.timeline is not None: + scanner_timelines = timelines + + # ensure we have the events we need + config = deepcopy(config) + timeline = config.content.timeline + if config.content.events is None: + if timeline == "all" or timeline is True: + config.content.events = "all" + elif isinstance(timeline, list): + config.content.events = list(set(timeline) | {"span_begin", "span_end"}) + elif config.content.events != "all": + config.content.events = list( + set(config.content.events) | {"span_begin", "span_end"} + ) + + else: + scanner_timelines = [] + if config.content.events == "all": scanner_events = list(events) elif isinstance(config.content.events, list): @@ -158,4 +187,4 @@ def _scanner_messages_and_events( else: raise TypeError(f"Unexpected type for events: {type(config.content.events)}") - return scanner_messages, scanner_events + return scanner_messages, scanner_events, scanner_timelines diff --git a/src/inspect_scout/_scanner/types.py b/src/inspect_scout/_scanner/types.py index 317f5d3ae..512aea6b5 100644 --- a/src/inspect_scout/_scanner/types.py +++ b/src/inspect_scout/_scanner/types.py @@ -2,6 +2,7 @@ from typing import Sequence, Union +from inspect_ai.event import Timeline from inspect_ai.event._event import Event from inspect_ai.model._chat_message import ChatMessage from typing_extensions import Literal @@ -14,7 +15,11 @@ Sequence[ChatMessage], Event, Sequence[Event], + Timeline, + Sequence[Timeline], ] """Union of all valid scanner input types.""" -ScannerInputNames = Literal["transcript", "event", "events", "message", "messages"] +ScannerInputNames = Literal[ + "transcript", "event", "events", "message", "messages", "timeline", "timelines" +] diff --git a/src/inspect_scout/_scanner/util.py b/src/inspect_scout/_scanner/util.py index 53b6558c2..ec3104e45 100644 --- a/src/inspect_scout/_scanner/util.py +++ b/src/inspect_scout/_scanner/util.py @@ -1,7 +1,7 @@ from typing import Sequence, cast from inspect_ai.analysis._dataframe.extract import auto_id -from inspect_ai.event import Event +from inspect_ai.event import Event, Timeline from inspect_ai.event._base import BaseEvent from inspect_ai.model import ChatMessage, ChatMessageBase @@ -16,7 +16,7 @@ def get_input_type_and_ids( Args: loader_result: Scanner input which can be a Transcript, ChatMessage, Event, - or a sequence of messages/events. + Timeline, or a sequence of messages/events/timelines. Returns: A tuple of (input type name, list of IDs) for the given input, or None if @@ -28,6 +28,8 @@ def get_input_type_and_ids( return ("message", [_message_id(loader_result)]) elif isinstance(loader_result, BaseEvent): return ("event", [_event_id(loader_result)]) + elif isinstance(loader_result, Timeline): + return ("timeline", [loader_result.name]) elif len(loader_result) == 0: return None elif isinstance(loader_result[0], ChatMessageBase): @@ -40,6 +42,12 @@ def get_input_type_and_ids( "events", [_event_id(evt) for evt in cast(Sequence[Event], loader_result)], ) + elif isinstance(loader_result[0], Timeline): + return ( + "timelines", + [t.name for t in cast(Sequence[Timeline], loader_result)], + ) + return None def _event_id(event: Event) -> str: diff --git a/src/inspect_scout/_scanner/validate.py b/src/inspect_scout/_scanner/validate.py index f518d4f31..bb465da2d 100644 --- a/src/inspect_scout/_scanner/validate.py +++ b/src/inspect_scout/_scanner/validate.py @@ -11,6 +11,7 @@ get_type_hints, ) +from inspect_ai.event import Timeline from inspect_ai.event._approval import ApprovalEvent from inspect_ai.event._compaction import CompactionEvent from inspect_ai.event._error import ErrorEvent @@ -70,16 +71,17 @@ def infer_filters_from_type( scanner_fn: Callable[..., Any], factory_globals: dict[str, Any], -) -> tuple[list[str] | None, list[str] | None]: +) -> tuple[list[str] | None, list[str] | None, bool]: """ - Infer message and event filters from scanner function type annotations. + Infer message, event, and timeline filters from scanner function type annotations. Args: scanner_fn: The scanner function to analyze factory_globals: Global namespace for type resolution Returns: - Tuple of (message_filters, event_filters) or (None, None) if can't infer + Tuple of (message_filters, event_filters, timeline_inferred) or + (None, None, False) if can't infer """ # Get type hints try: @@ -87,38 +89,46 @@ def infer_filters_from_type( scanner_fn, globalns=factory_globals, localns=factory_globals ) except Exception: - return None, None + return None, None, False # Get the input parameter type param_names = list(inspect.signature(scanner_fn).parameters.keys()) if not param_names: - return None, None + return None, None, False input_param = param_names[0] if input_param not in hints: - return None, None + return None, None, False input_type = hints[input_param] # First check if it's ChatMessage, Event, or Transcript (even if they're unions) # We don't want to infer for these "base" types if input_type == ChatMessage or input_type == Event or input_type == Transcript: - return None, None + return None, None, False + + # Check for Timeline before unwrapping list + if input_type is Timeline: + return None, None, True # Extract the actual type (unwrap list if needed) if get_origin(input_type) is list: args = get_args(input_type) if args: - input_type = args[0] + element_type = args[0] + # Check for list[Timeline] + if element_type is Timeline: + return None, None, True + input_type = element_type # Check again for base types after unwrapping if ( input_type == ChatMessage or input_type == Event or input_type == Transcript ): - return None, None + return None, None, False else: - return None, None + return None, None, False # Check if it's a specific message type or union message_filters = [] @@ -141,22 +151,23 @@ def infer_filters_from_type( try: if inspect.isclass(input_type) and issubclass(input_type, Transcript): # It's Transcript, we don't have a specific filter for it - return None, None + return None, None, False except TypeError: # Not a class type pass - return None, None + return None, None, False # If we have both message and event filters, can't use inference # (should use Transcript instead) if message_filters and event_filters: - return None, None + return None, None, False # Return inferred filters # Type: ignore because mypy can't understand that the lists contain the right string literals return ( message_filters if message_filters else None, event_filters if event_filters else None, + False, ) @@ -165,6 +176,7 @@ def validate_scanner_signature( messages: list[MessageType] | Literal["all"] | None, events: list[EventType] | Literal["all"] | None, factory_globals: dict[str, Any], + timeline: list[EventType] | Literal["all"] | None = None, ) -> None: """ Validate that scanner function signature matches its declared filters. @@ -174,6 +186,7 @@ def validate_scanner_signature( messages: Message filter from decorator events: Event filter from decorator factory_globals: Global namespace from factory function for type resolution + timeline: Timeline filter from decorator Raises: TypeError: If scanner signature doesn't match filters @@ -202,6 +215,11 @@ def validate_scanner_signature( input_type = hints[input_param] + # Timeline filter present + if timeline is not None: + _validate_timeline_type(input_type, messages) + return + # Check what the scanner should accept based on filters if messages is not None and events is not None: # Both filters present - should accept Transcript @@ -227,6 +245,45 @@ def _validate_transcript_type( ) +def _validate_timeline_type( + scanner_type: Any, + messages: list[MessageType] | Literal["all"] | None, +) -> None: + """Validate that scanner accepts Timeline, list[Timeline], or Transcript. + + Only checks messages to determine if Transcript is required. A scanner + with timeline + implied events can accept Timeline directly. + """ + # Transcript is always valid when timeline is requested + if _is_compatible_with_type(scanner_type, Transcript): + return + + # Check for Timeline directly + if _is_compatible_with_type(scanner_type, Timeline): + # Timeline with explicit message filters requires Transcript + if messages is not None: + raise TypeError( + f"Scanner with timeline and messages filters must accept Transcript, " + f"but scanner accepts {scanner_type}" + ) + return + + # Check for list[Timeline] + is_list, core_type = _unwrap_list_type(scanner_type) + if is_list and _is_compatible_with_type(core_type, Timeline): + if messages is not None: + raise TypeError( + f"Scanner with timeline and messages filters must accept Transcript, " + f"but scanner accepts {scanner_type}" + ) + return + + raise TypeError( + f"Scanner with timeline filter must accept Timeline, list[Timeline], or Transcript, " + f"but scanner accepts {scanner_type}" + ) + + def _validate_message_type( scanner_type: Any, message_filter: list[MessageType] | Literal["all"], diff --git a/src/inspect_scout/_scanner_ir/generator.py b/src/inspect_scout/_scanner_ir/generator.py index 78d85eae7..e3c3dafdf 100644 --- a/src/inspect_scout/_scanner_ir/generator.py +++ b/src/inspect_scout/_scanner_ir/generator.py @@ -261,6 +261,12 @@ def _generate_decorator(spec: ScannerDecoratorSpec) -> str: if spec.version != 0: args.append(f"version={spec.version}") + if spec.timeline: + if spec.timeline == "all": + args.append('timeline="all"') + else: + args.append(f"timeline={spec.timeline!r}") + if args: return f"@scanner({', '.join(args)})" else: @@ -287,6 +293,9 @@ def _generate_llm_scanner_call(spec: LLMScannerSpec) -> str: if spec.model: args.append(f'model="{spec.model}"') + if spec.model_role: + args.append(f'model_role="{spec.model_role}"') + if spec.retry_refusals is not None and spec.retry_refusals != 3: args.append(f"retry_refusals={spec.retry_refusals}") @@ -298,6 +307,21 @@ def _generate_llm_scanner_call(spec: LLMScannerSpec) -> str: else: args.append(f'template="{template}"') + if spec.name: + args.append(f'name="{spec.name}"') + + if spec.context_window is not None: + args.append(f"context_window={spec.context_window}") + + if spec.compaction is not None: + if isinstance(spec.compaction, str): + args.append(f'compaction="{spec.compaction}"') + else: + args.append(f"compaction={spec.compaction}") + + if spec.depth is not None: + args.append(f"depth={spec.depth}") + # Format nicely if len(args) <= 2 and all(len(a) < 40 for a in args): return f"llm_scanner({', '.join(args)})" @@ -317,7 +341,8 @@ def _generate_answer_arg(spec: LLMScannerSpec) -> str: case "multi_labels": labels_repr = repr(spec.labels) - return f"AnswerMultiLabel(labels={labels_repr})" + none_arg = ", allow_none=True" if spec.allow_none else "" + return f"AnswerMultiLabel(labels={labels_repr}{none_arg})" case "structured": if spec.structured_spec: @@ -469,6 +494,22 @@ def _update_decorator(self, decorator: cst.Decorator) -> cst.Decorator: ) ) + if spec.timeline: + if spec.timeline == "all": + new_args.append( + cst.Arg( + keyword=cst.Name("timeline"), + value=cst.SimpleString('"all"'), + ) + ) + else: + new_args.append( + cst.Arg( + keyword=cst.Name("timeline"), + value=_list_to_cst(spec.timeline), + ) + ) + if new_args: new_call = cst.Call(func=cst.Name("scanner"), args=new_args) return decorator.with_changes(decorator=new_call) @@ -518,6 +559,14 @@ def _update_llm_scanner_call(self, call: cst.Call) -> cst.Call: ) ) + if spec.model_role: + new_args.append( + cst.Arg( + keyword=cst.Name("model_role"), + value=cst.SimpleString(f'"{spec.model_role}"'), + ) + ) + if spec.retry_refusals is not None and spec.retry_refusals != 3: new_args.append( cst.Arg( @@ -544,6 +593,46 @@ def _update_llm_scanner_call(self, call: cst.Call) -> cst.Call: ) ) + if spec.name: + new_args.append( + cst.Arg( + keyword=cst.Name("name"), + value=cst.SimpleString(f'"{spec.name}"'), + ) + ) + + if spec.context_window is not None: + new_args.append( + cst.Arg( + keyword=cst.Name("context_window"), + value=cst.Integer(str(spec.context_window)), + ) + ) + + if spec.compaction is not None: + if isinstance(spec.compaction, str): + new_args.append( + cst.Arg( + keyword=cst.Name("compaction"), + value=cst.SimpleString(f'"{spec.compaction}"'), + ) + ) + else: + new_args.append( + cst.Arg( + keyword=cst.Name("compaction"), + value=cst.Integer(str(spec.compaction)), + ) + ) + + if spec.depth is not None: + new_args.append( + cst.Arg( + keyword=cst.Name("depth"), + value=cst.Integer(str(spec.depth)), + ) + ) + return call.with_changes(args=new_args) def _update_grep_scanner_call(self, call: cst.Call) -> cst.Call: @@ -648,14 +737,22 @@ def _answer_to_cst(spec: LLMScannerSpec) -> cst.BaseExpression: case "multi_labels": if spec.labels: - return cst.Call( - func=cst.Name("AnswerMultiLabel"), - args=[ + args = [ + cst.Arg( + keyword=cst.Name("labels"), + value=_list_to_cst(spec.labels), + ) + ] + if spec.allow_none: + args.append( cst.Arg( - keyword=cst.Name("labels"), - value=_list_to_cst(spec.labels), + keyword=cst.Name("allow_none"), + value=cst.Name("True"), ) - ], + ) + return cst.Call( + func=cst.Name("AnswerMultiLabel"), + args=args, ) return cst.SimpleString('"boolean"') diff --git a/src/inspect_scout/_scanner_ir/parser.py b/src/inspect_scout/_scanner_ir/parser.py index bd0f76f01..db84895b4 100644 --- a/src/inspect_scout/_scanner_ir/parser.py +++ b/src/inspect_scout/_scanner_ir/parser.py @@ -58,6 +58,14 @@ def parse_scanner_file(source: str) -> ParseResult: # Parse decorator decorator_spec = _parse_scanner_decorator(scanner_func) + # Check if decorator has advanced-only params + if isinstance(decorator_spec, str): + return ParseResult( + editable=False, + source=source, + advanced_reason=decorator_spec, + ) + # Find the return statement with llm_scanner or grep_scanner call scanner_call, scanner_type = _find_scanner_call(scanner_func) if scanner_call is None: @@ -133,10 +141,19 @@ def _is_scanner_decorator(decorator: cst.Decorator) -> bool: return False -def _parse_scanner_decorator(func: cst.FunctionDef) -> ScannerDecoratorSpec: - """Parse the @scanner decorator arguments.""" +def _parse_scanner_decorator( + func: cst.FunctionDef, +) -> ScannerDecoratorSpec | str: + """Parse the @scanner decorator arguments. + + Returns: + ScannerDecoratorSpec if parsed successfully, or an advanced_reason string + if the decorator has parameters that make it non-editable. + """ spec = ScannerDecoratorSpec() + _known_decorator_params = {"messages", "events", "name", "version", "timeline"} + for decorator in func.decorators: if not _is_scanner_decorator(decorator): continue @@ -156,6 +173,12 @@ def _parse_scanner_decorator(func: cst.FunctionDef) -> ScannerDecoratorSpec: spec.name = value elif key == "version": spec.version = value + elif key == "timeline": + spec.timeline = value + elif key == "metrics": + return "Uses custom metrics" + elif key not in _known_decorator_params: + return f"Uses unsupported decorator parameter: {key}" return spec @@ -196,10 +219,16 @@ def _parse_llm_scanner_call( question: str | None = None answer_type: str | None = None labels: list[str] | None = None + allow_none: bool = False structured_spec: StructuredAnswerSpec | None = None model: str | None = None + model_role: str | None = None retry_refusals: int | None = None template: str | None = None + name: str | None = None + context_window: int | None = None + compaction: str | int | None = None + depth: int | None = None for arg in call.args: if arg.keyword is None: @@ -218,11 +247,14 @@ def _parse_llm_scanner_call( parsed = _parse_answer_arg(value, tree) if parsed is None: return None, "Could not parse answer argument" - answer_type, labels, structured_spec = parsed + answer_type, labels, structured_spec, allow_none = parsed elif key == "model": model = _eval_literal(value) + elif key == "model_role": + model_role = _eval_literal(value) + elif key == "retry_refusals": retry_refusals = _eval_literal(value) @@ -237,6 +269,30 @@ def _parse_llm_scanner_call( elif key == "preprocessor": return None, "Uses custom message preprocessor" + elif key == "name": + name = _eval_literal(value) + + elif key == "context_window": + context_window = _eval_literal(value) + + elif key == "compaction": + compaction = _eval_literal(value) + + elif key == "depth": + depth = _eval_literal(value) + + elif key == "value_to_float": + return None, "Uses custom value_to_float function" + + elif key == "content": + return None, "Uses custom content configuration" + + elif key == "reducer": + return None, "Uses custom reducer function" + + else: + return None, f"Uses unsupported parameter: {key}" + if question is None: return None, "Missing question argument" if answer_type is None: @@ -247,10 +303,16 @@ def _parse_llm_scanner_call( question=question, answer_type=answer_type, # type: ignore[arg-type] labels=labels, + allow_none=allow_none, structured_spec=structured_spec, model=model, + model_role=model_role, retry_refusals=retry_refusals, template=template, + name=name, + context_window=context_window, + compaction=compaction, + depth=depth, ), None, ) @@ -258,16 +320,16 @@ def _parse_llm_scanner_call( def _parse_answer_arg( value: cst.BaseExpression, tree: cst.Module -) -> tuple[str, list[str] | None, StructuredAnswerSpec | None] | None: +) -> tuple[str, list[str] | None, StructuredAnswerSpec | None, bool] | None: """Parse the answer= argument. - Returns (answer_type, labels, structured_spec) or None if unparseable. + Returns (answer_type, labels, structured_spec, allow_none) or None if unparseable. """ # Simple literal: "boolean", "numeric", "string" if _is_string_literal(value): literal = _eval_literal(value) if literal in ("boolean", "numeric", "string"): - return (literal, None, None) + return (literal, None, None, False) # List of labels: ["A", "B", "C"] if isinstance(value, cst.List): @@ -277,25 +339,32 @@ def _parse_answer_arg( labels.append(_eval_literal(el.value)) else: return None - return ("labels", labels, None) + return ("labels", labels, None, False) - # AnswerMultiLabel(labels=[...]) + # AnswerMultiLabel(labels=[...], allow_none=True/False) if isinstance(value, cst.Call): func_name = _get_call_name(value) if func_name == "AnswerMultiLabel": + multi_labels: list[str] | None = None + allow_none = False for arg in value.args: if arg.keyword and arg.keyword.value == "labels": if isinstance(arg.value, cst.List): - labels = [] + multi_labels = [] for el in arg.value.elements: if isinstance(el, cst.Element) and _is_string_literal( el.value ): - labels.append(_eval_literal(el.value)) + multi_labels.append(_eval_literal(el.value)) else: return None - return ("multi_labels", labels, None) + elif arg.keyword and arg.keyword.value == "allow_none": + allow_none = ( + isinstance(arg.value, cst.Name) and arg.value.value == "True" + ) + if multi_labels is not None: + return ("multi_labels", multi_labels, None, allow_none) # AnswerStructured(type=ModelName) or AnswerStructured(type=list[ModelName]) elif func_name == "AnswerStructured": @@ -307,7 +376,7 @@ def _parse_answer_arg( model_spec = _find_pydantic_model(tree, model_name) if model_spec: model_spec.is_list = is_list - return ("structured", None, model_spec) + return ("structured", None, model_spec, False) return None diff --git a/src/inspect_scout/_scanner_ir/types.py b/src/inspect_scout/_scanner_ir/types.py index 96e02b93c..e310758fe 100644 --- a/src/inspect_scout/_scanner_ir/types.py +++ b/src/inspect_scout/_scanner_ir/types.py @@ -39,10 +39,16 @@ class LLMScannerSpec(BaseModel): "boolean", "numeric", "string", "labels", "multi_labels", "structured" ] labels: list[str] | None = Field(default=None) + allow_none: bool = Field(default=False) structured_spec: StructuredAnswerSpec | None = Field(default=None) model: str | None = Field(default=None) + model_role: str | None = Field(default=None) retry_refusals: int | None = Field(default=None) template: str | None = Field(default=None) + name: str | None = Field(default=None) + context_window: int | None = Field(default=None) + compaction: str | int | None = Field(default=None) + depth: int | None = Field(default=None) # ============ Grep Scanner Types ============ @@ -70,6 +76,7 @@ class ScannerDecoratorSpec(BaseModel): events: list[str] | None = Field(default=None) name: str | None = Field(default=None) version: int = Field(default=0) + timeline: Literal["all"] | list[str] | None = Field(default=None) class ScannerFile(BaseModel): diff --git a/src/inspect_scout/_scanresults.py b/src/inspect_scout/_scanresults.py index 586a37092..5fd4e64de 100644 --- a/src/inspect_scout/_scanresults.py +++ b/src/inspect_scout/_scanresults.py @@ -4,6 +4,7 @@ import pandas as pd from inspect_ai._util._async import run_coroutine from inspect_ai._util.json import to_json_str_safe +from inspect_ai.log import expand_events from upath import UPath from ._recorder.factory import scan_recorder_type_for_location @@ -124,23 +125,25 @@ async def scan_results_df_async( scan_location, scanner=scanner, exclude_columns=exclude_columns ) - # Apply expansion lazily when in "results" mode + # Always expand condensed event refs (storage optimization, not consumer-visible) if rows == "results": - scanners = LazyScannerMapping( - scanner_names=list(results.scanners.keys()), - loader=lambda name: results.scanners[name], - transformer=_expand_resultset_rows, - ) - return ScanResultsDF( - complete=results.complete, - spec=results.spec, - location=results.location, - summary=results.summary, - errors=results.errors, - scanners=scanners, - ) + transformer = lambda df: _expand_resultset_rows(_expand_events_in_df(df)) # noqa: E731 + else: + transformer = _expand_events_in_df - return results + scanners = LazyScannerMapping( + scanner_names=list(results.scanners.keys()), + loader=lambda name: results.scanners[name], + transformer=transformer, + ) + return ScanResultsDF( + complete=results.complete, + spec=results.spec, + location=results.location, + summary=results.summary, + errors=results.errors, + scanners=scanners, + ) def scan_results_db( @@ -332,6 +335,32 @@ def assign_label_validation(row: pd.Series) -> pd.Series: return expanded, synthetic_rows +def _expand_events_in_df(df: pd.DataFrame) -> pd.DataFrame: + """Expand condensed event refs in the input column, then drop input_data.""" + if "input" not in df.columns or "input_data" not in df.columns or df["input_data"].isna().all(): + return df.drop(columns=["input_data"], errors="ignore") + + df = df.copy() + mask = df["input_data"].notna() + + for idx in df.index[mask]: + input_json = str(df.at[idx, "input"]) + input_data_json = str(df.at[idx, "input_data"]) + input_type = str(df.at[idx, "input_type"]) + + if input_type == "transcript": + transcript = json.loads(input_json) + events_json = json.dumps(transcript.get("events", [])) + expanded = expand_events(events_json, input_data_json) + transcript["events"] = [e.model_dump() for e in expanded] + df.at[idx, "input"] = json.dumps(transcript) + elif input_type == "events": + expanded = expand_events(input_json, input_data_json) + df.at[idx, "input"] = json.dumps([e.model_dump() for e in expanded]) + + return df.drop(columns=["input_data"]) + + def _expand_resultset_rows(df: pd.DataFrame) -> pd.DataFrame: """ Expand rows where value_type == "resultset" into multiple rows. diff --git a/src/inspect_scout/_transcript/caching.py b/src/inspect_scout/_transcript/caching.py index 56b7a2b37..bd33fccb5 100644 --- a/src/inspect_scout/_transcript/caching.py +++ b/src/inspect_scout/_transcript/caching.py @@ -14,7 +14,7 @@ from .types import LogPaths DEFAULT_MAX_CACHE_ENTRIES = 5000 -_CACHE_VERSION = 9 +_CACHE_VERSION = 10 _CACHE_VERSION_KEY = "__cache_version__" diff --git a/src/inspect_scout/_transcript/database/parquet/encryption.py b/src/inspect_scout/_transcript/database/parquet/encryption.py deleted file mode 100644 index e4f462cfb..000000000 --- a/src/inspect_scout/_transcript/database/parquet/encryption.py +++ /dev/null @@ -1,443 +0,0 @@ -import os -import shutil -import tempfile -from pathlib import Path - -import duckdb -from upath import UPath - -from inspect_scout._display import display -from inspect_scout._transcript.database.parquet.types import ( - ENCRYPTED_INDEX_EXTENSION, - IndexStorage, -) -from inspect_scout._util.filesystem import ensure_filesystem_dependencies - -# Environment variable for encryption key -ENCRYPTION_KEY_ENV = "SCOUT_DB_ENCRYPTION_KEY" - -# Internal DuckDB key name (used in PRAGMA add_parquet_key) -ENCRYPTION_KEY_NAME = "scout_key" - -# Valid AES key lengths in bytes (128, 192, 256 bits) -VALID_KEY_LENGTHS = (16, 24, 32) - - -def validate_encryption_key(key: str) -> None: - """Validate that an encryption key has a valid AES length. - - Args: - key: The encryption key to validate. - - Raises: - ValueError: If the key length is not valid for AES encryption. - """ - key_bytes = len(key.encode("utf-8")) - if key_bytes not in VALID_KEY_LENGTHS: - raise ValueError( - f"Invalid encryption key length: {key_bytes} bytes. " - f"AES keys must be 16, 24, or 32 bytes (128, 192, or 256 bits)." - ) - - -def get_encryption_key_from_env() -> str | None: - """Get encryption key from environment variable. - - Returns: - The encryption key if set, None otherwise. - """ - return os.environ.get(ENCRYPTION_KEY_ENV) - - -def _is_remote(location: str) -> bool: - """Check if location is a remote filesystem (S3 or HuggingFace).""" - return location.startswith("s3://") or location.startswith("hf://") - - -def _list_files_recursive(location: str) -> list[str]: - """List all files recursively in the given location. - - Args: - location: Local path or S3/HF URI. - - Returns: - List of file paths/URIs (directories excluded). - """ - location_path = UPath(location) - if not location_path.exists(): - return [] - # Get all files recursively, excluding directories - # UPath works for both local and remote (S3/HF) via fsspec - return [str(p) for p in location_path.rglob("*") if p.is_file()] - - -def _validate_output_dir(output_dir: str, overwrite: bool) -> None: - """Validate and prepare the output directory. - - Args: - output_dir: Path to output directory. - overwrite: If True, remove existing directory. - - Raises: - FileExistsError: If directory exists and overwrite is False. - """ - output_path = UPath(output_dir) - - if output_path.exists(): - if not overwrite: - raise FileExistsError( - f"Output directory already exists: {output_dir}. " - "Use --overwrite to replace it." - ) - # Remove existing directory - if _is_remote(output_dir): - # For remote, delete files recursively - for f in output_path.rglob("*"): - if f.is_file(): - f.unlink() - else: - shutil.rmtree(str(output_path)) - - output_path.mkdir(parents=True, exist_ok=True) - - -def _setup_duckdb(key: str) -> duckdb.DuckDBPyConnection: - """Create and configure a DuckDB connection with encryption key. - - Args: - key: The encryption key to register. - - Returns: - Configured DuckDB connection. - """ - conn = duckdb.connect(":memory:") - conn.execute("INSTALL httpfs") - conn.execute("LOAD httpfs") - conn.execute(f"PRAGMA add_parquet_key('{ENCRYPTION_KEY_NAME}', '{key}')") - return conn - - -def _get_relative_path(file_path: str, base_location: str) -> str: - """Get the relative path of a file from the base location. - - Args: - file_path: Full path to the file. - base_location: Base directory path. - - Returns: - Relative path from base to file. - """ - if _is_remote(base_location): - # For S3/HF, strip the base location prefix - base = base_location.rstrip("/") + "/" - if file_path.startswith(base): - return file_path[len(base) :] - return file_path - else: - return str(Path(file_path).relative_to(base_location)) - - -def _encrypt_parquet_file( - conn: duckdb.DuckDBPyConnection, - source_path: str, - dest_path: str, - output_is_remote: bool, -) -> None: - """Encrypt a parquet file using DuckDB footer_key encryption. - - Args: - conn: DuckDB connection with encryption key registered. - source_path: Path to source parquet file. - dest_path: Path for encrypted output file. - output_is_remote: Whether the output is on a remote filesystem. - """ - if output_is_remote: - # Write to temp file, then upload using UPath - with tempfile.NamedTemporaryFile(suffix=".enc.parquet", delete=False) as tmp: - tmp_path = tmp.name - try: - conn.execute( - f"COPY (SELECT * FROM read_parquet('{source_path}')) " - f"TO '{tmp_path}' (ENCRYPTION_CONFIG {{footer_key: '{ENCRYPTION_KEY_NAME}'}})" - ) - # Upload to remote using UPath - UPath(dest_path).write_bytes(Path(tmp_path).read_bytes()) - finally: - Path(tmp_path).unlink(missing_ok=True) - else: - # Write directly to local path - conn.execute( - f"COPY (SELECT * FROM read_parquet('{source_path}')) " - f"TO '{dest_path}' (ENCRYPTION_CONFIG {{footer_key: '{ENCRYPTION_KEY_NAME}'}})" - ) - - -def _decrypt_parquet_file( - conn: duckdb.DuckDBPyConnection, - source_path: str, - dest_path: str, - output_is_remote: bool, -) -> None: - """Decrypt a parquet file using DuckDB footer_key decryption. - - Args: - conn: DuckDB connection with encryption key registered. - source_path: Path to encrypted parquet file. - dest_path: Path for decrypted output file. - output_is_remote: Whether the output is on a remote filesystem. - """ - if output_is_remote: - # Write to temp file, then upload using UPath - with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as tmp: - tmp_path = tmp.name - try: - conn.execute( - f"COPY (SELECT * FROM read_parquet('{source_path}', " - f"encryption_config={{footer_key: '{ENCRYPTION_KEY_NAME}'}})) " - f"TO '{tmp_path}'" - ) - # Upload to remote using UPath - UPath(dest_path).write_bytes(Path(tmp_path).read_bytes()) - finally: - Path(tmp_path).unlink(missing_ok=True) - else: - # Write directly to local path - conn.execute( - f"COPY (SELECT * FROM read_parquet('{source_path}', " - f"encryption_config={{footer_key: '{ENCRYPTION_KEY_NAME}'}})) " - f"TO '{dest_path}'" - ) - - -def _copy_file(source_path: str, dest_path: str, output_is_remote: bool) -> None: - """Copy a non-parquet file from source to destination. - - Uses UPath for cross-filesystem compatibility. - - Args: - source_path: Path to source file. - dest_path: Path for destination file. - output_is_remote: Whether the output is on a remote filesystem. - """ - # Read content from source (UPath handles both local and remote) - content = UPath(source_path).read_bytes() - - # Write to destination - dest = UPath(dest_path) - if not output_is_remote: - # For local, ensure parent directory exists - Path(dest_path).parent.mkdir(parents=True, exist_ok=True) - dest.write_bytes(content) - - -def encrypt_database(location: str, output_dir: str, key: str, overwrite: bool) -> None: - """Encrypt a transcript database using DuckDB parquet encryption. - - Takes parquet files from location (recursively) and creates encrypted - versions in output_dir with .enc.parquet extension. Non-parquet files - are copied unchanged. Directory structure is preserved. - - Args: - location: Source database location (local path or S3/HF URI). - output_dir: Output directory for encrypted files. - key: The encryption key to use. - overwrite: If True, overwrite existing output directory. - - Raises: - FileExistsError: If output_dir exists and overwrite is False. - """ - # Ensure filesystem dependencies are available - ensure_filesystem_dependencies(location) - ensure_filesystem_dependencies(output_dir) - - # Validate and prepare output directory - _validate_output_dir(output_dir, overwrite) - - # List all files in source - files = _list_files_recursive(location) - if not files: - return - - # Setup DuckDB connection - conn = _setup_duckdb(key) - - output_is_remote = _is_remote(output_dir) - - with display().text_progress("Encrypting", len(files)) as progress: - for file_path in files: - file_name = UPath(file_path).name - progress.update(text=file_name) - - # Compute relative path and destination - rel_path = _get_relative_path(file_path, location) - - if file_path.endswith(".parquet") and not file_path.endswith( - ".enc.parquet" - ): - # Encrypt parquet file - dest_rel_path = rel_path.replace(".parquet", ".enc.parquet") - if output_is_remote: - dest_path = f"{output_dir.rstrip('/')}/{dest_rel_path}" - else: - dest_path = str(UPath(output_dir) / dest_rel_path) - # Ensure parent directory exists for local - Path(dest_path).parent.mkdir(parents=True, exist_ok=True) - - _encrypt_parquet_file(conn, file_path, dest_path, output_is_remote) - else: - # Copy non-parquet file unchanged - if output_is_remote: - dest_path = f"{output_dir.rstrip('/')}/{rel_path}" - else: - dest_path = str(UPath(output_dir) / rel_path) - - _copy_file(file_path, dest_path, output_is_remote) - - conn.close() - - -def decrypt_database(location: str, output_dir: str, key: str, overwrite: bool) -> None: - """Decrypt a transcript database using DuckDB parquet decryption. - - Takes encrypted parquet files (.enc.parquet) from location (recursively) - and creates decrypted versions in output_dir with .parquet extension. - Non-encrypted files are copied unchanged. Directory structure is preserved. - - Args: - location: Source database location (local path or S3/HF URI). - output_dir: Output directory for decrypted files. - key: The encryption key to use. - overwrite: If True, overwrite existing output directory. - - Raises: - FileExistsError: If output_dir exists and overwrite is False. - """ - # Ensure filesystem dependencies are available - ensure_filesystem_dependencies(location) - ensure_filesystem_dependencies(output_dir) - - # Validate and prepare output directory - _validate_output_dir(output_dir, overwrite) - - # List all files in source - files = _list_files_recursive(location) - if not files: - return - - # Setup DuckDB connection - conn = _setup_duckdb(key) - - output_is_remote = _is_remote(output_dir) - - with display().text_progress("Decrypting", len(files)) as progress: - for file_path in files: - file_name = UPath(file_path).name - progress.update(text=file_name) - - # Compute relative path and destination - rel_path = _get_relative_path(file_path, location) - - if file_path.endswith(".enc.parquet"): - # Decrypt encrypted parquet file - dest_rel_path = rel_path.replace(".enc.parquet", ".parquet") - if output_is_remote: - dest_path = f"{output_dir.rstrip('/')}/{dest_rel_path}" - else: - dest_path = str(UPath(output_dir) / dest_rel_path) - # Ensure parent directory exists for local - Path(dest_path).parent.mkdir(parents=True, exist_ok=True) - - _decrypt_parquet_file(conn, file_path, dest_path, output_is_remote) - else: - # Copy non-encrypted file unchanged - if output_is_remote: - dest_path = f"{output_dir.rstrip('/')}/{rel_path}" - else: - dest_path = str(UPath(output_dir) / rel_path) - - _copy_file(file_path, dest_path, output_is_remote) - - conn.close() - - -def _is_encrypted_index_file(filename: str) -> bool: - """Check if an index file is encrypted based on its extension.""" - return filename.endswith(ENCRYPTED_INDEX_EXTENSION) - - -def _check_index_encryption_status(index_files: list[str]) -> bool | None: - """Check encryption status of index files and validate consistency. - - Args: - index_files: List of index file paths. - - Returns: - True if all encrypted, False if all unencrypted, None if empty list. - - Raises: - ValueError: If index contains mixed encrypted and unencrypted files. - """ - if not index_files: - return None - - encrypted_count = sum(1 for f in index_files if _is_encrypted_index_file(f)) - unencrypted_count = len(index_files) - encrypted_count - - if encrypted_count > 0 and unencrypted_count > 0: - raise ValueError( - f"Index contains mixed encrypted ({encrypted_count}) and " - f"unencrypted ({unencrypted_count}) index files. " - "All index files must be either encrypted or unencrypted." - ) - - return encrypted_count > 0 - - -def _check_data_encryption_status(data_files: list[str]) -> bool | None: - """Check encryption status of data files and validate consistency. - - Args: - data_files: List of data file paths. - - Returns: - True if all encrypted, False if all unencrypted, None if empty list. - - Raises: - ValueError: If database contains mixed encrypted and unencrypted files. - """ - if not data_files: - return None - - encrypted_count = sum(1 for f in data_files if f.endswith(".enc.parquet")) - unencrypted_count = len(data_files) - encrypted_count - - if encrypted_count > 0 and unencrypted_count > 0: - raise ValueError( - f"Database contains mixed encrypted ({encrypted_count}) and " - f"unencrypted ({unencrypted_count}) parquet files. " - "All files must be either encrypted or unencrypted." - ) - - return encrypted_count > 0 - - -def setup_encryption( - conn: duckdb.DuckDBPyConnection, - storage: IndexStorage, -) -> str: - """Setup encryption key and return config string for read_parquet. - - Args: - conn: DuckDB connection to register the key with. - storage: Storage configuration with is_encrypted flag. - - Returns: - Encryption config string for read_parquet (empty if not encrypted). - """ - if not storage.is_encrypted: - return "" - - conn.execute( - f"PRAGMA add_parquet_key('{ENCRYPTION_KEY_NAME}', '{storage.encryption_key}')" - ) - return f", encryption_config={{footer_key: '{ENCRYPTION_KEY_NAME}'}}" diff --git a/src/inspect_scout/_transcript/database/parquet/index.py b/src/inspect_scout/_transcript/database/parquet/index.py index 4d09c8588..7b6bd36dc 100644 --- a/src/inspect_scout/_transcript/database/parquet/index.py +++ b/src/inspect_scout/_transcript/database/parquet/index.py @@ -4,19 +4,16 @@ enable fast metadata queries without scanning all data files. Index files are stored in an `_index/` directory with two types: -- Incremental index files: `index__.idx` (or `.enc.idx` if encrypted) +- Incremental index files: `index__.idx` Written during insert operations, one per batch. -- Compacted manifests: `_manifest__.idx` (or `.enc.idx` if encrypted) +- Compacted manifests: `_manifest__.idx` Written during compaction, consolidates multiple index files. The discovery priority ensures concurrent operations work correctly: 1. If any `_manifest_*.idx` exists, use the newest one 2. Also include any `index_*.idx` files newer than that manifest 3. If no manifest exists, use all `index_*.idx` files - -Encryption status is determined by file extensions (`.enc.idx` vs `.idx`), -mirroring the `.enc.parquet` vs `.parquet` pattern for data files. """ import glob @@ -36,16 +33,10 @@ from shortuuid import uuid from upath import UPath -from .encryption import ( - ENCRYPTION_KEY_NAME, - _check_data_encryption_status, - _check_index_encryption_status, - setup_encryption, -) +from ..schema import CONTENT_COLUMNS from .index_cache import get_index_cache_path, load_cached_index, save_index_cache from .migration import migrate_table from .types import ( - ENCRYPTED_INDEX_EXTENSION, INCREMENTAL_PREFIX, INDEX_DIR, INDEX_EXTENSION, @@ -59,9 +50,8 @@ # Regex patterns for extracting timestamps from filenames # UUID part allows any alphanumeric (actual UUIDs are hex, but be permissive for testing) -# These patterns match both encrypted (.enc.idx) and unencrypted (.idx) files -INCREMENTAL_PATTERN = re.compile(r"index_(\d{8}T\d{6})_[a-zA-Z0-9]+(?:\.enc)?\.idx$") -MANIFEST_PATTERN = re.compile(r"_manifest_(\d{8}T\d{6})_[a-zA-Z0-9]+(?:\.enc)?\.idx$") +INCREMENTAL_PATTERN = re.compile(r"index_(\d{8}T\d{6})_[a-zA-Z0-9]+\.idx$") +MANIFEST_PATTERN = re.compile(r"_manifest_(\d{8}T\d{6})_[a-zA-Z0-9]+\.idx$") async def append_index( @@ -85,12 +75,12 @@ async def append_index( if storage.is_remote(): # Remote storage: write to temp file then upload with tempfile.NamedTemporaryFile( - suffix=storage.index_extension(), delete=False + suffix=INDEX_EXTENSION, delete=False ) as tmp_file: tmp_path = tmp_file.name try: - _write_parquet_table(table, tmp_path, storage) + _write_parquet_table(table, tmp_path) # Upload to remote assert storage.fs is not None, "AsyncFilesystem required for remote storage" @@ -103,11 +93,11 @@ async def append_index( output_path.parent.mkdir(parents=True, exist_ok=True) # Write to temp file in same directory (ensures same filesystem for rename) - tmp_filename = f".tmp_{uuid()[:8]}{storage.index_extension()}" + tmp_filename = f".tmp_{uuid()[:8]}{INDEX_EXTENSION}" local_tmp_path = output_path.parent / tmp_filename try: - _write_parquet_table(table, str(local_tmp_path), storage) + _write_parquet_table(table, str(local_tmp_path)) # Atomic rename on POSIX - prevents partial files from being visible os.rename(local_tmp_path, output_path) except Exception: @@ -126,9 +116,14 @@ async def compact_index( """Compact multiple index files into one. Steps: - 1. Read all index files into merged manifest + 1. Read all discovered index files into merged manifest 2. Write single compacted manifest file - 3. (Only after success) Delete ALL old index files + 3. (Only after success) Delete the merged index files + + Only files discovered at the start of compaction are deleted. Files + written concurrently by other sessions survive for the next compaction, + preventing a race condition where parallel writers' index entries could + be permanently lost. Args: conn: DuckDB connection. @@ -139,10 +134,10 @@ async def compact_index( CompactionResult with stats about files merged/deleted. """ MAX_RETRIES = 3 - # Use discovery for reading (gets the right files to merge) + # Use discovery for reading (gets the right files to merge). + # Only these files will be deleted after compaction — files written + # concurrently by other sessions will survive for the next compaction. idx_files = await _discover_index_files(storage) - # List ALL files for cleanup (includes orphaned older files) - all_idx_files = await _list_all_index_files(storage) if not idx_files: # No index files at all @@ -151,8 +146,6 @@ async def compact_index( index_files_deleted=0, new_index_path="", ) - # Setup encryption if needed (discover_index_files sets storage.is_encrypted) - enc_config = setup_encryption(conn, storage) # Load all index files into a single table with deduplication. # If the same transcript_id appears in multiple index files (from retried @@ -160,7 +153,7 @@ async def compact_index( # Index files have timestamps in their names, so newer files sort later. # We tag each file with its position in the sorted list (_file_order). try: - merged_table = _read_and_deduplicate_index_files(conn, idx_files, enc_config) + merged_table = _read_and_deduplicate_index_files(conn, idx_files) except duckdb.IOException as e: error_msg = str(e).lower() if ( @@ -174,12 +167,12 @@ async def compact_index( raise # Write compacted manifest (even if only 1 file - converts incremental to manifest) - new_filename = _generate_manifest_filename(encrypted=storage.is_encrypted) + new_filename = _generate_manifest_filename() new_path = await append_index(merged_table, storage, new_filename) - # Delete ALL old index files (only after successful write) + # Delete merged index files (only after successful write) deleted_idx_count = 0 - for old_file in all_idx_files: + for old_file in idx_files: try: await _delete_file(storage, old_file) deleted_idx_count += 1 @@ -207,8 +200,6 @@ async def create_index( After successfully writing the new index, any existing index files are deleted. This ensures corrupted or partial indexes are cleaned up. - The index will be encrypted if the data files are encrypted. - Args: conn: DuckDB connection. storage: Storage configuration. @@ -231,18 +222,15 @@ async def create_index( else: file_pattern = "[" + ", ".join(f"'{p}'" for p in data_files) + "]" - # Setup encryption if needed (discover_data_files sets storage.is_encrypted) - enc_config = setup_encryption(conn, storage) - # Read all metadata from data files, excluding messages/events # First, get the schema to know which columns exist schema_result = conn.execute( - f"SELECT column_name FROM (DESCRIBE SELECT * FROM read_parquet({file_pattern}, union_by_name=true{enc_config}))" + f"SELECT column_name FROM (DESCRIBE SELECT * FROM read_parquet({file_pattern}, union_by_name=true))" ).fetchall() all_columns = {row[0] for row in schema_result} - # Build exclude clause for messages/events if they exist - exclude_columns = [c for c in ["messages", "events"] if c in all_columns] + # Build exclude clause for large content columns if they exist + exclude_columns = [c for c in CONTENT_COLUMNS if c in all_columns] exclude_clause = ( f" EXCLUDE ({', '.join(exclude_columns)})" if exclude_columns else "" ) @@ -255,7 +243,7 @@ async def create_index( SELECT * EXCLUDE (_rn) FROM ( SELECT *{exclude_clause}, ROW_NUMBER() OVER (PARTITION BY transcript_id) as _rn - FROM read_parquet({file_pattern}, union_by_name=true, filename=true{enc_config}) + FROM read_parquet({file_pattern}, union_by_name=true, filename=true) ) WHERE _rn = 1 """).fetch_arrow_table() @@ -264,7 +252,7 @@ async def create_index( result = _make_filenames_relative(result, storage.location) # Write as manifest file (this is a full rebuild, so use manifest naming) - filename = _generate_manifest_filename(encrypted=storage.is_encrypted) + filename = _generate_manifest_filename() new_path = await append_index(result, storage, filename) # Delete old index files (only after successful write) @@ -306,7 +294,6 @@ async def init_index_table( Raises: duckdb.Error: If index files are corrupted or unreadable. - ValueError: If encrypted but no encryption key available. """ MAX_RETRIES = 3 idx_files = await _discover_index_files(storage) @@ -326,7 +313,7 @@ async def init_index_table( cache_path = get_index_cache_path(storage, idx_files) # Try loading from cache - cached_count = load_cached_index(conn, cache_path, table_name, storage) + cached_count = load_cached_index(conn, cache_path, table_name) if cached_count is not None: trace_message( logger, "Scout Index Cache", f"Loaded from cache: {cache_path}" @@ -341,15 +328,12 @@ async def init_index_table( else: file_pattern = "[" + ", ".join(f"'{p}'" for p in idx_files) + "]" - # Setup encryption if needed (discover_index_files sets storage.is_encrypted) - enc_config = setup_encryption(conn, storage) - # Create table from index files # Handle file-not-found errors from concurrent operations with retry try: conn.execute(f""" CREATE TABLE {table_name} AS - SELECT * FROM read_parquet({file_pattern}, union_by_name=true{enc_config}) + SELECT * FROM read_parquet({file_pattern}, union_by_name=true) """) except duckdb.IOException as e: error_msg = str(e).lower() @@ -370,7 +354,7 @@ async def init_index_table( # For remote storage, save to cache if storage.is_remote() and cache_path is not None: try: - save_index_cache(conn, cache_path, table_name, storage) + save_index_cache(conn, cache_path, table_name) trace_message(logger, "Scout Index Cache", f"Saved to cache: {cache_path}") except Exception as e: logger.warning(f"Failed to save index cache: {e}") @@ -394,9 +378,6 @@ async def _discover_index_files(storage: IndexStorage) -> list[str]: Returns: List of index file paths to use. - - Raises: - ValueError: If index contains mixed encrypted and unencrypted files. """ index_dir = storage.index_dir_path() @@ -410,7 +391,7 @@ async def _discover_index_files(storage: IndexStorage) -> list[str]: return [] all_idx_files = [f.name for f in all_files if _is_index_file(f.name)] else: - # Local storage: use glob for both extensions + # Local storage: use glob index_path = UPath(index_dir) if not index_path.exists(): return [] @@ -419,9 +400,6 @@ async def _discover_index_files(storage: IndexStorage) -> list[str]: if not all_idx_files: return [] - # Validate encryption status consistency (raises if mixed) - _check_index_encryption_status(all_idx_files) - # Separate manifests from incremental files manifest_files = [ f for f in all_idx_files if Path(f).name.startswith(MANIFEST_PREFIX) @@ -442,15 +420,11 @@ async def _discover_index_files(storage: IndexStorage) -> list[str]: # Shouldn't happen, but fall back to just using manifest return [newest_manifest] - # Include incremental files at or after the manifest timestamp - # Using >= handles the case where insert happens in the same second as compaction - newer_incrementals = [] - for f in incremental_files: - inc_ts = _extract_timestamp(f) - if inc_ts and inc_ts >= manifest_ts: - newer_incrementals.append(f) - - return [newest_manifest] + sorted(newer_incrementals) + # Include ALL remaining incremental files alongside the manifest. + # With the compact_index fix that only deletes merged files, any + # remaining incrementals are exactly the un-merged ones. The dedup + # logic in _read_and_deduplicate_index_files handles any overlaps. + return [newest_manifest] + sorted(incremental_files) async def _list_all_index_files(storage: IndexStorage) -> list[str]: @@ -489,21 +463,7 @@ async def _discover_data_files(storage: IndexStorage) -> list[str]: Returns: List of data file paths. - - Raises: - ValueError: If database contains mixed encrypted and unencrypted files. """ - data_files = await _discover_data_files_internal(storage) - - # Validate encryption status consistency (raises if mixed) - if data_files: - _check_data_encryption_status(data_files) - - return data_files - - -async def _discover_data_files_internal(storage: IndexStorage) -> list[str]: - """Internal helper to discover data files without encryption detection.""" if storage.is_remote(): fs = filesystem(storage.location) all_files = fs.ls(storage.location, recursive=True) @@ -528,41 +488,20 @@ async def _discover_data_files_internal(storage: IndexStorage) -> list[str]: def _write_parquet_table( table: pa.Table, path: str, - storage: IndexStorage, ) -> None: """Write PyArrow table to Parquet with standard settings. - Uses DuckDB to write encrypted parquet when encryption is enabled. - Args: table: PyArrow table to write. path: Destination file path. - storage: Storage configuration with is_encrypted flag. """ - if storage.is_encrypted: - conn = duckdb.connect() - try: - conn.execute( - f"PRAGMA add_parquet_key('{ENCRYPTION_KEY_NAME}', '{storage.encryption_key}')" - ) - # Register the PyArrow table and write with encryption - conn.register("source_table", table) - conn.execute( - f"COPY source_table TO '{path}' " - f"(FORMAT PARQUET, COMPRESSION 'zstd', " - f"ENCRYPTION_CONFIG {{footer_key: '{ENCRYPTION_KEY_NAME}'}})" - ) - finally: - conn.close() - else: - # Write unencrypted using PyArrow - pq.write_table( - table, - path, - compression="zstd", - use_dictionary=True, - write_statistics=True, - ) + pq.write_table( + table, + path, + compression="zstd", + use_dictionary=True, + write_statistics=True, + ) async def _delete_file(storage: IndexStorage, path: str) -> None: @@ -585,8 +524,8 @@ async def _delete_file(storage: IndexStorage, path: str) -> None: def _is_index_file(filename: str) -> bool: - """Check if a file is an index file (encrypted or not).""" - return filename.endswith(INDEX_EXTENSION) # Matches both .idx and .enc.idx + """Check if a file is an index file.""" + return filename.endswith(INDEX_EXTENSION) def _extract_timestamp(filename: str) -> str | None: @@ -618,7 +557,7 @@ def _generate_timestamp() -> str: return datetime.now(timezone.utc).strftime(TIMESTAMP_FORMAT) -def _generate_manifest_filename(encrypted: bool = False) -> str: +def _generate_manifest_filename() -> str: """Generate filename for a compacted manifest file. Includes short UUID to ensure uniqueness when multiple compactions @@ -626,8 +565,7 @@ def _generate_manifest_filename(encrypted: bool = False) -> str: """ timestamp = _generate_timestamp() unique_id = uuid()[:8] - ext = ENCRYPTED_INDEX_EXTENSION if encrypted else INDEX_EXTENSION - return f"{MANIFEST_PREFIX}{timestamp}_{unique_id}{ext}" + return f"{MANIFEST_PREFIX}{timestamp}_{unique_id}{INDEX_EXTENSION}" def _make_filenames_relative(table: pa.Table, location: str) -> pa.Table: @@ -670,7 +608,6 @@ def _make_filenames_relative(table: pa.Table, location: str) -> pa.Table: def _read_and_deduplicate_index_files( conn: duckdb.DuckDBPyConnection, idx_files: list[str], - enc_config: str, ) -> pa.Table: """Read index files and deduplicate by transcript_id. @@ -682,7 +619,6 @@ def _read_and_deduplicate_index_files( Args: conn: DuckDB connection. idx_files: List of index file paths (should be sorted by timestamp). - enc_config: DuckDB encryption config string. Returns: PyArrow table with deduplicated entries. @@ -690,7 +626,7 @@ def _read_and_deduplicate_index_files( if len(idx_files) == 1: # Single file - no deduplication needed return conn.execute( - f"SELECT * FROM read_parquet('{idx_files[0]}'{enc_config})" + f"SELECT * FROM read_parquet('{idx_files[0]}')" ).fetch_arrow_table() # Read each file with an order tag. Higher order = newer file. @@ -698,9 +634,7 @@ def _read_and_deduplicate_index_files( # the list index as the order (newer files have higher indices). subqueries = [] for i, f in enumerate(sorted(idx_files)): - subqueries.append( - f"SELECT *, {i} as _file_order FROM read_parquet('{f}'{enc_config})" - ) + subqueries.append(f"SELECT *, {i} as _file_order FROM read_parquet('{f}')") union_query = " UNION ALL BY NAME ".join(subqueries) diff --git a/src/inspect_scout/_transcript/database/parquet/index_cache.py b/src/inspect_scout/_transcript/database/parquet/index_cache.py index b11818ae2..de31df190 100644 --- a/src/inspect_scout/_transcript/database/parquet/index_cache.py +++ b/src/inspect_scout/_transcript/database/parquet/index_cache.py @@ -8,7 +8,7 @@ ~/.cache/inspect_scout/index_cache/ └── v{VERSION}/ # Version directory (bump to invalidate all caches) ├── / # One directory per remote database - │ └── .parquet # Cache file (or .enc.parquet) + │ └── .parquet # Cache file └── ... Cache invalidation: @@ -16,7 +16,6 @@ - Each remote location gets its own subdirectory (hash of URL) - Cache key is a hash of sorted index filenames - When index files change (added, removed, compacted), the hash changes -- Encrypted databases get encrypted caches (.enc.parquet) """ import hashlib @@ -27,7 +26,6 @@ from inspect_scout._util.appdirs import scout_cache_dir -from .encryption import ENCRYPTION_KEY_NAME, setup_encryption from .types import IndexStorage # Cache version - bump this to invalidate all existing caches @@ -86,15 +84,13 @@ def get_index_cache_path(storage: IndexStorage, index_files: list[str]) -> Path: cache_dir = _location_cache_dir(storage) cache_dir.mkdir(parents=True, exist_ok=True) cache_key = _index_cache_key(index_files) - ext = ".enc.parquet" if storage.is_encrypted else ".parquet" - return cache_dir / f"{cache_key}{ext}" + return cache_dir / f"{cache_key}.parquet" def load_cached_index( conn: duckdb.DuckDBPyConnection, cache_path: Path, table_name: str, - storage: IndexStorage, ) -> int | None: """Load index from local cache if it exists. @@ -102,7 +98,6 @@ def load_cached_index( conn: DuckDB connection. cache_path: Path to the cache file. table_name: Name for the table to create. - storage: Storage configuration (for encryption). Returns: Row count if cache was loaded, None if cache doesn't exist or is invalid. @@ -111,10 +106,9 @@ def load_cached_index( return None try: - enc_config = setup_encryption(conn, storage) conn.execute(f""" CREATE TABLE {table_name} AS - SELECT * FROM read_parquet('{cache_path}'{enc_config}) + SELECT * FROM read_parquet('{cache_path}') """) result = conn.execute(f"SELECT COUNT(*) FROM {table_name}").fetchone() return result[0] if result else 0 @@ -128,7 +122,6 @@ def save_index_cache( conn: duckdb.DuckDBPyConnection, cache_path: Path, table_name: str, - storage: IndexStorage, ) -> None: """Save index table to local cache. @@ -139,7 +132,6 @@ def save_index_cache( conn: DuckDB connection with the table loaded. cache_path: Path to write the cache file. table_name: Name of the table to save. - storage: Storage configuration (for encryption). """ # Skip if another process already wrote the cache while we were loading if cache_path.exists(): @@ -149,19 +141,10 @@ def save_index_cache( tmp_path = cache_path.with_suffix(".tmp") try: - if storage.is_encrypted: - # Use DuckDB COPY with encryption - conn.execute(f""" - COPY {table_name} TO '{tmp_path}' - (FORMAT PARQUET, COMPRESSION 'zstd', - ENCRYPTION_CONFIG {{footer_key: '{ENCRYPTION_KEY_NAME}'}}) - """) - else: - # Write unencrypted - conn.execute(f""" - COPY {table_name} TO '{tmp_path}' - (FORMAT PARQUET, COMPRESSION 'zstd') - """) + conn.execute(f""" + COPY {table_name} TO '{tmp_path}' + (FORMAT PARQUET, COMPRESSION 'zstd') + """) # Atomic rename - on POSIX this atomically replaces any existing file os.rename(tmp_path, cache_path) diff --git a/src/inspect_scout/_transcript/database/parquet/transcripts.py b/src/inspect_scout/_transcript/database/parquet/transcripts.py index 6c18f4a92..ef408a3ab 100644 --- a/src/inspect_scout/_transcript/database/parquet/transcripts.py +++ b/src/inspect_scout/_transcript/database/parquet/transcripts.py @@ -15,9 +15,10 @@ import pyarrow.compute as pc import pyarrow.parquet as pq from inspect_ai._util.asyncfiles import AsyncFilesystem -from inspect_ai._util.error import PrerequisiteError from inspect_ai._util.file import filesystem +from inspect_ai._util.json import to_json_str_safe from inspect_ai._util.path import pretty_path +from inspect_ai.log import condense_events, expand_events from inspect_ai.util import trace_action, trace_message from typing_extensions import override from upath import UPath @@ -46,13 +47,7 @@ ) from ..database import TranscriptsDB from ..reader import TranscriptsViewReader -from ..schema import TRANSCRIPT_SCHEMA_FIELDS, reserved_columns -from .encryption import ( - ENCRYPTION_KEY_ENV, - ENCRYPTION_KEY_NAME, - get_encryption_key_from_env, - validate_encryption_key, -) +from ..schema import CONTENT_COLUMNS, TRANSCRIPT_SCHEMA_FIELDS, reserved_columns from .index import ( _discover_index_files, append_index, @@ -61,7 +56,7 @@ init_index_table, ) from .migration import migrate_view -from .types import IndexStorage +from .types import INDEX_EXTENSION, IndexStorage logger = getLogger(__name__) @@ -81,23 +76,14 @@ def __init__( self, parquet_path: str, transcript_id: str, - enc_config: str, ) -> None: self._parquet_path = parquet_path self._transcript_id = transcript_id - self._enc_config = enc_config self._conn: duckdb.DuckDBPyConnection | None = None async def __aenter__(self) -> "_ParquetStreamContextManager": # Create fresh connection for streaming self._conn = duckdb.connect(":memory:") - # Re-acquire encryption key from environment (same as connect() does) - if self._enc_config: # enc_config is non-empty only for encrypted dbs - key = get_encryption_key_from_env() - if key: - self._conn.execute( - f"PRAGMA add_parquet_key('{ENCRYPTION_KEY_NAME}', '{key}')" - ) return self async def __aexit__(self, *_: object) -> None: @@ -129,15 +115,50 @@ async def _stream_chunks(self) -> AsyncIterator[bytes]: assert self._conn is not None try: - sql = f"SELECT messages, events FROM read_parquet(?, union_by_name=true{self._enc_config}) WHERE transcript_id = ?" + sql = ( + "SELECT messages, events, events_data, timelines" + " FROM read_parquet(?, union_by_name=true)" + " WHERE transcript_id = ?" + ) result = self._conn.execute( sql, [self._parquet_path, self._transcript_id] ).fetchone() except duckdb.BinderException: - result = None - - messages_json: str | None = result[0] if result else None - events_json: str | None = result[1] if result else None + # Old file missing events_data/timelines columns — retry without + try: + sql = ( + "SELECT messages, events, events_data" + " FROM read_parquet(?, union_by_name=true)" + " WHERE transcript_id = ?" + ) + result = self._conn.execute( + sql, [self._parquet_path, self._transcript_id] + ).fetchone() + except duckdb.BinderException: + try: + sql = ( + "SELECT messages, events" + " FROM read_parquet(?, union_by_name=true)" + " WHERE transcript_id = ?" + ) + result = self._conn.execute( + sql, [self._parquet_path, self._transcript_id] + ).fetchone() + except duckdb.BinderException: + result = None + + messages_json: str | None = None + events_json: str | None = None + events_data_json: str | None = None + timelines_json: str | None = None + + if result: + messages_json = result[0] + events_json = result[1] + if len(result) > 2: + events_data_json = result[2] + if len(result) > 3: + timelines_json = result[3] yield b'{"messages": ' if messages_json: @@ -155,6 +176,22 @@ async def _stream_chunks(self) -> AsyncIterator[bytes]: else: yield b"[]" + yield b', "events_data": ' + if events_data_json: + ed_bytes = events_data_json.encode("utf-8") + for i in range(0, len(ed_bytes), CHUNK_SIZE): + yield ed_bytes[i : i + CHUNK_SIZE] + else: + yield b"null" + + if timelines_json: + yield b', "timelines": ' + timelines_bytes = timelines_json.encode("utf-8") + for i in range(0, len(timelines_bytes), CHUNK_SIZE): + yield timelines_bytes[i : i + CHUNK_SIZE] + else: + yield b', "timelines": []' + yield b"}" @@ -176,9 +213,10 @@ def __init__( self, location: str, target_file_size_mb: float = 100, - row_group_size_mb: float = 32, + row_group_size: int = 25, query: Query | None = None, snapshot: ScanTranscripts | None = None, + pool_dedup: bool = True, ) -> None: """Initialize Parquet transcript database. @@ -186,19 +224,24 @@ def __init__( location: Directory path (local or S3) containing Parquet files. target_file_size_mb: Target size in MB for each Parquet file. Individual transcripts may cause files to exceed this limit. Can be fractional. - row_group_size_mb: Target row group size in MB for Parquet files. Can be fractional. + row_group_size: Target row group size as a row count for Parquet files. + Each transcript row can be 10-50 MB of JSON, so a small count + (default 25) keeps row groups safely under Parquet's 2GB limit. query: Optional query to apply during table creation for optimization. If provided, WHERE conditions are pushed down to Parquet scan, and SHUFFLE/LIMIT are applied during table creation. Query-time filters are additive (AND combination). snapshot: Snapshot info. This is a mapping of transcript_id => filename which we can use to avoid crawling. + pool_dedup: Condense repeated messages/calls into pools on write. + Exposed for testing; production callers should leave as True. """ self._location = location self._target_file_size_mb = target_file_size_mb - self._row_group_size_mb = row_group_size_mb + self._row_group_size = row_group_size self._query = query self._snapshot = snapshot + self._pool_dedup = pool_dedup # could be called in a spawed worker where there are no fs deps yet ensure_filesystem_dependencies(location) @@ -216,7 +259,7 @@ def __init__( self._file_columns_cache: dict[str, set[str]] = {} self._parquet_pattern: str | None = None self._exclude_clause: str = "" - self._is_encrypted: bool = False + self._parquet_columns: set[str] = set() @override async def connect(self) -> None: @@ -261,10 +304,9 @@ async def connect(self) -> None: self._init_hf_auth() # Initialize index storage for index operations - self._index_storage = await IndexStorage.create( + self._index_storage = IndexStorage.create( location=self._location, fs=self._fs, - key=get_encryption_key_from_env(), ) # Discover and register Parquet files @@ -365,8 +407,9 @@ async def commit(self, session_id: str | None = None) -> None: except Exception as e: logger.warning(f"Index compaction failed (data is consistent): {e}") - # Refresh the view AFTER compaction to reflect the final state - await self._create_transcripts_table() + # Refresh the view AFTER compaction to reflect the final state. + # Skip coverage check — transient staleness from parallel writers is expected. + await self._create_transcripts_table(check_coverage=False) @override async def select(self, query: Query | None = None) -> AsyncIterator[TranscriptInfo]: @@ -552,16 +595,25 @@ async def transcript_ids(self, query: Query | None = None) -> dict[str, str | No return transcript_ids def _get_content_size(self, full_path: str, transcript_id: str) -> int: - """Get decompressed size of messages+events columns for a transcript.""" + """Get decompressed size of messages+events+events_data columns for a transcript.""" assert self._conn is not None - enc_config = self._read_parquet_encryption_config() - result = self._conn.execute( - f""" - SELECT COALESCE(LENGTH(messages), 0) + COALESCE(LENGTH(events), 0) - FROM read_parquet(?{enc_config}) WHERE transcript_id = ? - """, - [full_path, transcript_id], - ).fetchone() + try: + result = self._conn.execute( + """ + SELECT COALESCE(LENGTH(messages), 0) + COALESCE(LENGTH(events), 0) + + COALESCE(LENGTH(events_data), 0) + FROM read_parquet(?) WHERE transcript_id = ? + """, + [full_path, transcript_id], + ).fetchone() + except duckdb.BinderException: + result = self._conn.execute( + """ + SELECT COALESCE(LENGTH(messages), 0) + COALESCE(LENGTH(events), 0) + FROM read_parquet(?) WHERE transcript_id = ? + """, + [full_path, transcript_id], + ).fetchone() return result[0] if result else 0 @override @@ -613,8 +665,9 @@ def transcript_no_content() -> Transcript: # Determine which columns we need to read need_messages = content.messages is not None need_events = content.events is not None + need_timelines = content.timeline is not None - if not need_messages and not need_events: + if not need_messages and not need_events and not need_timelines: # No content needed - use model_construct to preserve LazyJSONDict return transcript_no_content() @@ -624,6 +677,11 @@ def transcript_no_content() -> Transcript: columns.append("messages") if need_events: columns.append("events") + if need_timelines: + columns.append("timelines") + # events_data piggybacks on events — needed for pool resolution + if need_events: + columns.append("events_data") # First, get the filename from the index table (fast indexed lookup) filename_result = self._conn.execute( @@ -650,9 +708,8 @@ def transcript_no_content() -> Transcript: ) # Try optimistic read first (fast path for files with all columns) - enc_config = self._read_parquet_encryption_config() try: - sql = f"SELECT {', '.join(columns)} FROM read_parquet(?, union_by_name=true{enc_config}) WHERE transcript_id = ?" + sql = f"SELECT {', '.join(columns)} FROM read_parquet(?, union_by_name=true) WHERE transcript_id = ?" result = self._conn.execute( sql, [full_path, t.transcript_id] ).fetchone() @@ -667,7 +724,7 @@ def transcript_no_content() -> Transcript: return transcript_no_content() # Retry with only available columns - sql = f"SELECT {', '.join(columns_read)} FROM read_parquet(?, union_by_name=true{enc_config}) WHERE transcript_id = ?" + sql = f"SELECT {', '.join(columns_read)} FROM read_parquet(?, union_by_name=true) WHERE transcript_id = ?" result = self._conn.execute( sql, [full_path, t.transcript_id] ).fetchone() @@ -679,6 +736,8 @@ def transcript_no_content() -> Transcript: # Extract column values based on which columns were actually read messages_json: str | None = None events_json: str | None = None + timelines_json: str | None = None + events_data_json: str | None = None col_idx = 0 if "messages" in columns_read: @@ -686,6 +745,13 @@ def transcript_no_content() -> Transcript: col_idx += 1 if "events" in columns_read: events_json = result[col_idx] + col_idx += 1 + if "timelines" in columns_read: + timelines_json = result[col_idx] + col_idx += 1 + if "events_data" in columns_read: + events_data_json = result[col_idx] + col_idx += 1 # Stream combined JSON construction async def stream_content_bytes() -> AsyncIterator[bytes]: @@ -708,16 +774,43 @@ async def stream_content_bytes() -> AsyncIterator[bytes]: else: yield b"[]" + if timelines_json: + yield b', "timelines": ' + timelines_bytes = timelines_json.encode("utf-8") + for i in range(0, len(timelines_bytes), chunk_size): + yield timelines_bytes[i : i + chunk_size] + yield b"}" # Use existing streaming JSON parser with filtering - return await load_filtered_transcript( + transcript = await load_filtered_transcript( stream_content_bytes(), t, content.messages, content.events, ) + # Resolve pool references back to full messages/calls + if events_data_json: + resolved_events = expand_events(transcript.events, events_data_json) + transcript = transcript.model_copy(update={"events": resolved_events}) + + # Fallback: if timelines were requested but not stored, build from events + if ( + content.timeline is not None + and not transcript.timelines + and transcript.events + ): + from inspect_ai.event import timeline_build + + from ...util import filter_timelines + + raw_timeline = timeline_build(transcript.events) + timelines = filter_timelines([raw_timeline], content.timeline) + transcript = transcript.model_copy(update={"timelines": timelines}) + + return transcript + @override async def read_messages_events( self, t: TranscriptInfo @@ -744,11 +837,8 @@ async def read_messages_events( # Get size upfront for API limit checks (must happen now) content_size = self._get_content_size(full_path, t.transcript_id) - # Capture encryption config now (view may close before streaming) - enc_config = self._read_parquet_encryption_config() - return TranscriptMessagesAndEvents( - data=_ParquetStreamContextManager(full_path, t.transcript_id, enc_config), + data=_ParquetStreamContextManager(full_path, t.transcript_id), compression_method=None, uncompressed_size=content_size, ) @@ -767,7 +857,7 @@ async def _insert_from_transcripts( progress.update(text=transcript.transcript_id) # Serialize once for both size calculation and writing - row = self._transcript_to_row(transcript) + row = self._transcript_to_row(transcript, pool_dedup=self._pool_dedup) row_size = self._estimate_row_size(row) # Add transcript ID for duplicate tracking @@ -858,21 +948,35 @@ async def _insert_from_record_batch_reader( if accumulated_batches: await self._write_arrow_batch(accumulated_batches, session_id) - def _transcript_to_row(self, transcript: Transcript) -> dict[str, Any]: + def _transcript_to_row( + self, transcript: Transcript, *, pool_dedup: bool = True + ) -> dict[str, Any]: """Convert Transcript to Parquet row dict with flattened metadata. Args: transcript: Transcript to convert. + pool_dedup: Condense repeated messages/calls into pools. Returns: Dict with Parquet column values. """ + from inspect_ai.event import timeline_dump + # Validate metadata keys don't conflict with reserved names _validate_metadata_keys(transcript.metadata) - # Serialize messages and events as JSON arrays + # Serialize messages as JSON array messages_array = [msg.model_dump() for msg in transcript.messages] - events_array = [event.model_dump() for event in transcript.events] + + if pool_dedup: + condensed_events, events_data = condense_events(transcript.events) + events_json = to_json_str_safe(condensed_events) + events_data_json = to_json_str_safe(events_data) + else: + events_json = json.dumps( + [event.model_dump() for event in transcript.events] + ) + events_data_json = None # Start with reserved fields row: dict[str, Any] = { @@ -904,7 +1008,13 @@ def _transcript_to_row(self, transcript: Transcript) -> dict[str, Any]: "error": transcript.error, "limit": transcript.limit, "messages": json.dumps(messages_array), - "events": json.dumps(events_array), + "events": events_json, + "timelines": ( + json.dumps([timeline_dump(tl) for tl in transcript.timelines]) + if transcript.timelines + else None + ), + "events_data": events_data_json, } # Flatten metadata: add each key as a column @@ -942,7 +1052,7 @@ def _estimate_row_size(self, row: dict[str, Any]) -> int: if value is None: continue # NULL values have minimal overhead elif isinstance(value, str): - if key in ("messages", "events"): + if key in CONTENT_COLUMNS: json_array_size += len(value) else: other_size += len(value) @@ -972,7 +1082,7 @@ def _estimate_batch_size(self, batch: pa.RecordBatch) -> int: for i, name in enumerate(batch.schema.names): col_size = batch.column(i).nbytes - if name in ("messages", "events"): + if name in CONTENT_COLUMNS: json_array_size += col_size else: other_size += col_size @@ -1009,8 +1119,10 @@ def _validate_record_batch_schema(self, schema: pa.Schema) -> None: col_type = schema.field(field.name).type expected_type = field.pyarrow_type - # String columns: allow large_string as equivalent - if expected_type == pa.string(): + # String columns: allow both string and large_string (backward compat) + if pa.types.is_string(expected_type) or pa.types.is_large_string( + expected_type + ): if col_type not in (pa.string(), pa.large_string()): raise ValueError( f"'{field.name}' column must be string type, got {col_type}" @@ -1066,9 +1178,8 @@ def _get_available_content_columns(self, filename: str) -> set[str]: """ if filename not in self._file_columns_cache: assert self._conn is not None - enc_config = self._read_parquet_encryption_config() schema_result = self._conn.execute( - f"SELECT column_name FROM (DESCRIBE SELECT * FROM read_parquet(?{enc_config}))", + "SELECT column_name FROM (DESCRIBE SELECT * FROM read_parquet(?))", [filename], ).fetchall() self._file_columns_cache[filename] = {row[0] for row in schema_result} @@ -1086,7 +1197,7 @@ def _write_parquet_file(self, table: pa.Table, path: str) -> None: path, compression="zstd", use_dictionary=True, - row_group_size=int(self._row_group_size_mb * 1024 * 1024), + row_group_size=self._row_group_size, write_statistics=True, ) @@ -1217,14 +1328,14 @@ def _infer_column_type(self, key: str, rows: list[dict[str, Any]]) -> pa.DataTyp values = [row.get(key) for row in rows if row.get(key) is not None] if not values: - return pa.string() # All NULL → default to string + return pa.large_string() # All NULL → default to large string # Determine types present types = {type(v) for v in values} # Infer appropriate PyArrow type if types == {str}: - return pa.string() + return pa.large_string() elif types == {bool}: return pa.bool_() elif types == {int}: @@ -1238,8 +1349,8 @@ def _infer_column_type(self, key: str, rows: list[dict[str, Any]]) -> pa.DataTyp # Mix of numeric types → use float return pa.float64() else: - # Mixed incompatible types → use string - return pa.string() + # Mixed incompatible types → use large string + return pa.large_string() async def _write_parquet_batch( self, batch: list[dict[str, Any]], session_id: str | None = None @@ -1260,9 +1371,10 @@ async def _write_parquet_batch( # Infer schema from actual data schema = self._infer_schema(batch) - # Create DataFrame and convert to PyArrow table + # Use inferred schema (which promotes strings to large_string) + # so Arrow uses 64-bit string offsets. df = pd.DataFrame(batch) - table = pa.Table.from_pandas(df, schema=schema) + table = pa.Table.from_pandas(df, schema=schema, preserve_index=False) # Generate filename and write to storage filename = self._generate_parquet_filename(session_id) @@ -1271,7 +1383,7 @@ async def _write_parquet_batch( # Write index file for this batch await self._write_index_for_batch(table, parquet_path, filename) - async def _create_transcripts_table(self) -> None: + async def _create_transcripts_table(self, check_coverage: bool = True) -> None: """Create DuckDB structures for querying transcripts. Creates: @@ -1306,14 +1418,17 @@ async def _create_transcripts_table(self) -> None: idx_files = await _discover_index_files(self._index_storage) # Skip index warnings when snapshot is provided - workers already have - # efficient access via the transcript_id->filename mapping from parent + # efficient access via the transcript_id->filename mapping from parent. + # Also skip during commit (check_coverage=False) since transient + # staleness from parallel writers is expected. has_snapshot = bool(self._snapshot and self._snapshot.transcript_ids) + should_check = check_coverage and not has_snapshot if idx_files: - await self._init_from_index(check_coverage=not has_snapshot) + await self._init_from_index(check_coverage=should_check) else: # Initialize from parquet files (warning is issued inside if files exist) - await self._init_from_parquet(warn_missing_index=not has_snapshot) + await self._init_from_parquet(warn_missing_index=should_check) async def _init_from_index(self, check_coverage: bool = False) -> None: """Initialize from index files (fast path). @@ -1338,6 +1453,9 @@ async def _init_from_index(self, check_coverage: bool = False) -> None: self._create_empty_structures() return + # Synthesize missing schema columns as NULL + self._ensure_index_schema() + # Create index for fast lookups self._conn.execute( "CREATE INDEX idx_transcript_id ON transcript_index(transcript_id)" @@ -1360,6 +1478,11 @@ async def _init_from_index(self, check_coverage: bool = False) -> None: ): self._apply_query_filter_to_tables() + def _ensure_index_schema(self) -> None: + """Add missing schema columns to transcript_index table.""" + assert self._conn is not None + _ensure_index_schema(self._conn) + async def _init_from_parquet(self, warn_missing_index: bool = True) -> None: """Initialize from parquet files (legacy/slow path). @@ -1398,15 +1521,14 @@ async def _init_from_parquet(self, warn_missing_index: bool = True) -> None: f"Queries will be slower. Run `scout db index {pretty_path(self._location)}` to build an index." ) - # Setup encryption if needed - self._setup_encryption(file_paths) - # Build pattern for read_parquet pattern = self._build_parquet_pattern(file_paths) self._parquet_pattern = pattern # Infer exclude clause from first file - self._exclude_clause = self._infer_exclude_clause(file_paths[0]) + self._exclude_clause, self._parquet_columns = self._infer_exclude_clause( + file_paths[0] + ) # Create transcript_index table (id + filename only) if self._snapshot and self._snapshot.transcript_ids: @@ -1478,7 +1600,7 @@ def _create_empty_structures(self) -> None: for field in TRANSCRIPT_SCHEMA_FIELDS: duckdb_type = _pyarrow_to_duckdb_type(field.pyarrow_type) default_value = _duckdb_default_value(field.pyarrow_type) - column_defs.append(f"{default_value}::{duckdb_type} AS {field.name}") + column_defs.append(f'{default_value}::{duckdb_type} AS "{field.name}"') # Add filename column (internal) column_defs.append("''::VARCHAR AS filename") @@ -1490,28 +1612,6 @@ def _create_empty_structures(self) -> None: WHERE FALSE """) - def _setup_encryption(self, file_paths: list[str]) -> None: - """Detect and configure encryption if needed.""" - assert self._conn is not None - - # Check encryption status (validates no mixed encrypted/unencrypted) - self._is_encrypted = self._check_encryption_status(file_paths) - - if self._is_encrypted: - key = get_encryption_key_from_env() - if not key: - raise PrerequisiteError( - f"Encrypted database detected but no encryption key provided. " - f"Set the {ENCRYPTION_KEY_ENV} environment variable." - ) - try: - validate_encryption_key(key) - except ValueError as e: - raise PrerequisiteError(str(e)) from e - self._conn.execute( - f"PRAGMA add_parquet_key('{ENCRYPTION_KEY_NAME}', '{key}')" - ) - def _build_parquet_pattern(self, file_paths: list[str]) -> str: """Build DuckDB pattern string for read_parquet.""" if len(file_paths) == 1: @@ -1538,10 +1638,9 @@ def _create_index_from_parquet(self, pattern: str) -> None: """Create transcript_index table by querying parquet files.""" assert self._conn is not None - enc_config = self._read_parquet_encryption_config() base_sql = f""" SELECT transcript_id, filename - FROM read_parquet({pattern}, union_by_name=true, filename=true{enc_config}) + FROM read_parquet({pattern}, union_by_name=true, filename=true) """ # Apply pre-filter query if provided @@ -1557,7 +1656,7 @@ def _create_index_from_parquet(self, pattern: str) -> None: index_sql = f"CREATE TABLE transcript_index AS {base_sql}" self._conn.execute(index_sql, params) - def _infer_exclude_clause(self, file_path: str) -> str: + def _infer_exclude_clause(self, file_path: str) -> tuple[str, set[str]]: """Infer EXCLUDE clause from a single file's schema. Reads schema from one file (fast - only reads Parquet footer metadata) @@ -1567,24 +1666,20 @@ def _infer_exclude_clause(self, file_path: str) -> str: file_path: Path to a Parquet file to sample. Returns: - EXCLUDE clause string (e.g., " EXCLUDE (messages, events)") or empty string. + Tuple of (EXCLUDE clause string, set of existing column names). """ assert self._conn is not None - enc_config = self._read_parquet_encryption_config() schema_result = self._conn.execute( - f"SELECT column_name FROM (DESCRIBE SELECT * FROM read_parquet('{file_path}'{enc_config}))" + f"SELECT column_name FROM (DESCRIBE SELECT * FROM read_parquet('{file_path}'))" ).fetchall() existing_columns = {row[0] for row in schema_result} - exclude_columns = [ - col for col in ["messages", "events"] if col in existing_columns - ] + exclude_columns = [col for col in CONTENT_COLUMNS if col in existing_columns] - if exclude_columns: - return f" EXCLUDE ({', '.join(exclude_columns)})" - return "" + clause = f" EXCLUDE ({', '.join(exclude_columns)})" if exclude_columns else "" + return clause, existing_columns - def _infer_exclude_clause_full(self, pattern: str) -> str: + def _infer_exclude_clause_full(self, pattern: str) -> tuple[str, set[str]]: """Infer EXCLUDE clause by scanning all files' schemas. Slower fallback that unions schemas from all files to handle @@ -1594,21 +1689,35 @@ def _infer_exclude_clause_full(self, pattern: str) -> str: pattern: DuckDB file pattern for read_parquet. Returns: - EXCLUDE clause string or empty string. + Tuple of (EXCLUDE clause string, set of existing column names). """ assert self._conn is not None - enc_config = self._read_parquet_encryption_config() schema_result = self._conn.execute( - f"SELECT column_name FROM (DESCRIBE SELECT * FROM read_parquet({pattern}, union_by_name=true{enc_config}))" + f"SELECT column_name FROM (DESCRIBE SELECT * FROM read_parquet({pattern}, union_by_name=true))" ).fetchall() existing_columns = {row[0] for row in schema_result} - exclude_columns = [ - col for col in ["messages", "events"] if col in existing_columns - ] + exclude_columns = [col for col in CONTENT_COLUMNS if col in existing_columns] - if exclude_columns: - return f" EXCLUDE ({', '.join(exclude_columns)})" + clause = f" EXCLUDE ({', '.join(exclude_columns)})" if exclude_columns else "" + return clause, existing_columns + + def _missing_columns_clause(self) -> str: + """Generate SQL for schema columns missing from parquet files. + + Produces NULL-typed expressions for optional schema fields not present + in the parquet data, ensuring the VIEW always has a complete schema. + """ + missing_exprs: list[str] = [] + for field in TRANSCRIPT_SCHEMA_FIELDS: + if ( + field.name not in self._parquet_columns + and field.name not in CONTENT_COLUMNS + ): + duckdb_type = _pyarrow_to_duckdb_type(field.pyarrow_type) + missing_exprs.append(f'NULL::{duckdb_type} AS "{field.name}"') + if missing_exprs: + return ", " + ", ".join(missing_exprs) return "" def _create_transcripts_view(self, pattern: str) -> None: @@ -1622,10 +1731,8 @@ def _create_transcripts_view(self, pattern: str) -> None: """ assert self._conn is not None - enc_config = self._read_parquet_encryption_config() - # Build VIEW SQL based on whether pre-filter was applied - def build_view_sql(exclude_clause: str) -> str: + def build_view_sql(exclude_clause: str, missing_clause: str) -> str: if self._snapshot or ( self._query and (self._query.where or self._query.shuffle or self._query.limit) @@ -1633,25 +1740,31 @@ def build_view_sql(exclude_clause: str) -> str: # VIEW joins with pre-filtered index table return f""" CREATE VIEW transcripts AS - SELECT p.*{exclude_clause} - FROM read_parquet({pattern}, union_by_name=true, filename=true{enc_config}) p + SELECT p.*{exclude_clause}{missing_clause} + FROM read_parquet({pattern}, union_by_name=true, filename=true) p INNER JOIN transcript_index i ON p.transcript_id = i.transcript_id """ else: # No pre-filter - VIEW directly queries Parquet return f""" CREATE VIEW transcripts AS - SELECT *{exclude_clause} - FROM read_parquet({pattern}, union_by_name=true, filename=true{enc_config}) + SELECT *{exclude_clause}{missing_clause} + FROM read_parquet({pattern}, union_by_name=true, filename=true) """ + missing_clause = self._missing_columns_clause() + # Try with exclude clause from first file (fast path) try: - self._conn.execute(build_view_sql(self._exclude_clause)) + self._conn.execute(build_view_sql(self._exclude_clause, missing_clause)) except duckdb.BinderException: # Schema differs across files - fall back to full scan - self._exclude_clause = self._infer_exclude_clause_full(pattern) - self._conn.execute(build_view_sql(self._exclude_clause)) + self._conn.execute("DROP VIEW IF EXISTS transcripts") + self._exclude_clause, self._parquet_columns = ( + self._infer_exclude_clause_full(pattern) + ) + missing_clause = self._missing_columns_clause() + self._conn.execute(build_view_sql(self._exclude_clause, missing_clause)) # migrate view for databases imported from eval_log migrate_view(self._conn, "transcripts") @@ -1687,65 +1800,41 @@ async def _discover_parquet_files(self) -> list[str]: str(p) for p in location_path.glob(f"**/{PARQUET_TRANSCRIPTS_GLOB}") ] - def _check_encryption_status(self, file_paths: list[str]) -> bool: - """Check if database files are encrypted and validate consistency. - - Args: - file_paths: List of parquet file paths. - - Returns: - True if all files are encrypted, False if all unencrypted. - - Raises: - ValueError: If database contains a mix of encrypted and unencrypted files. - """ - encrypted_count = sum(1 for f in file_paths if f.endswith(".enc.parquet")) - unencrypted_count = len(file_paths) - encrypted_count - - if encrypted_count > 0 and unencrypted_count > 0: - raise ValueError( - f"Database contains mixed encrypted ({encrypted_count}) and " - f"unencrypted ({unencrypted_count}) parquet files. " - "All files must be either encrypted or unencrypted." - ) - - return encrypted_count > 0 - - def _read_parquet_encryption_config(self) -> str: - """Get encryption config string for read_parquet calls. - - Returns: - Empty string if not encrypted, or encryption config parameter. - """ - if self._is_encrypted: - return f", encryption_config={{footer_key: '{ENCRYPTION_KEY_NAME}'}}" - return "" - def _have_transcript(self, transcript_id: str) -> bool: return transcript_id in (self._transcript_ids or set()) def _index_filename_for_parquet(self, parquet_filename: str) -> str: """Generate index filename matching parquet file's timestamp/uuid. + Strips any session ID prefix so the index filename always follows + ``index_{timestamp}_{uuid}.idx`` format. The session ID is only needed + in parquet filenames (for ``_list_session_files``); the index filename + must use just the timestamp+uuid portion so that alphabetical ordering + gives compacted (session-free) entries higher ``_file_order`` than + their session-scoped predecessors during deduplication. + Args: - parquet_filename: Name of the parquet file (e.g., transcripts_20250101T120000_abc123.parquet) + parquet_filename: Name of the parquet file. + With session: ``transcripts_{session_id}_{timestamp}_{uuid}.parquet`` + Without session: ``transcripts_{timestamp}_{uuid}.parquet`` Returns: - Index filename (e.g., index_20250101T120000_abc123.idx or .enc.idx if encrypted) + Index filename (e.g., ``index_20250101T120000_abc123.idx``) """ - assert self._index_storage is not None - # Extract timestamp_uuid from: transcripts_20250101T120000_abc123.parquet - # or transcripts_20250101T120000_abc123.enc.parquet - base = Path(parquet_filename).stem # transcripts_20250101T120000_abc123 - if base.endswith(".enc"): - base = base[:-4] # Remove .enc suffix - # Remove "transcripts_" prefix, keep timestamp_uuid - timestamp_uuid = base.replace("transcripts_", "", 1) - ext = self._index_storage.index_extension() - return f"index_{timestamp_uuid}{ext}" + base = Path(parquet_filename).stem + # Remove "transcripts_" prefix + remainder = base.replace("transcripts_", "", 1) + # Strip optional session_id prefix: if remainder doesn't start with a + # digit (timestamp format YYYYMMDDThhmmss), it has a session_id to remove + parts = remainder.split("_") + for i, part in enumerate(parts): + if part[0:1].isdigit() and "T" in part: + remainder = "_".join(parts[i:]) + break + return f"index_{remainder}{INDEX_EXTENSION}" def _build_index_table(self, table: pa.Table, parquet_filename: str) -> pa.Table: - """Build index table from data table (excludes messages/events, adds filename). + """Build index table from data table (excludes large content columns, adds filename). Args: table: PyArrow table with full transcript data. @@ -1754,9 +1843,9 @@ def _build_index_table(self, table: pa.Table, parquet_filename: str) -> pa.Table Returns: Index table with metadata columns and filename. """ - # Get columns to keep (exclude messages and events) + # Get columns to keep (exclude large content columns) columns_to_keep = [ - name for name in table.column_names if name not in ("messages", "events") + name for name in table.column_names if name not in CONTENT_COLUMNS ] # Select only metadata columns @@ -1810,7 +1899,7 @@ async def _compact_session(self, session_id: str) -> None: """Compact all parquet files from a session. Uses existing write logic which respects target_file_size_mb and - row_group_size_mb, potentially creating multiple output files for + row_group_size, potentially creating multiple output files for large sessions. Safe at every step - if interrupted, data remains queryable. @@ -1853,11 +1942,10 @@ async def _compact_session(self, session_id: str) -> None: ): # 2. Read all session data via DuckDB pattern = self._build_parquet_pattern(session_files) - enc_config = self._read_parquet_encryption_config() # Query and get a RecordBatchReader for streaming result = self._conn.execute(f""" - SELECT * FROM read_parquet({pattern}, union_by_name=true{enc_config}) + SELECT * FROM read_parquet({pattern}, union_by_name=true) """) # 3. Write to new file(s) WITHOUT session_id using existing logic @@ -2159,9 +2247,32 @@ def _validate_metadata_keys(metadata: dict[str, Any]) -> None: ) +def _ensure_index_schema(conn: duckdb.DuckDBPyConnection) -> None: + """Add missing schema columns to a transcript_index table. + + Ensures the table has all non-content schema columns, even when loaded + from older index files that predate newer columns. + + Args: + conn: DuckDB connection with a transcript_index table. + """ + existing = { + row[0] + for row in conn.execute( + "SELECT column_name FROM (DESCRIBE transcript_index)" + ).fetchall() + } + for field in TRANSCRIPT_SCHEMA_FIELDS: + if field.name not in existing and field.name not in CONTENT_COLUMNS: + duckdb_type = _pyarrow_to_duckdb_type(field.pyarrow_type) + conn.execute( + f'ALTER TABLE transcript_index ADD COLUMN "{field.name}" {duckdb_type}' + ) + + def _pyarrow_to_duckdb_type(pa_type: pa.DataType) -> str: """Convert PyArrow type to DuckDB SQL type string.""" - if pa_type == pa.string(): + if pa_type in (pa.string(), pa.large_string()): return "VARCHAR" elif pa_type == pa.int64(): return "BIGINT" @@ -2179,7 +2290,7 @@ def _pyarrow_to_duckdb_type(pa_type: pa.DataType) -> str: def _duckdb_default_value(pa_type: pa.DataType) -> str: """Get default value literal for a PyArrow type in DuckDB.""" - if pa_type == pa.string(): + if pa_type in (pa.string(), pa.large_string()): return "''" elif pa_type in (pa.int64(), pa.int32()): return "0" diff --git a/src/inspect_scout/_transcript/database/parquet/types.py b/src/inspect_scout/_transcript/database/parquet/types.py index 73936f6d4..242ef721b 100644 --- a/src/inspect_scout/_transcript/database/parquet/types.py +++ b/src/inspect_scout/_transcript/database/parquet/types.py @@ -3,16 +3,12 @@ from dataclasses import dataclass from inspect_ai._util.asyncfiles import AsyncFilesystem -from inspect_ai._util.error import PrerequisiteError -from inspect_ai._util.file import filesystem -from upath import UPath # Index directory and file patterns INDEX_DIR = "_index" INCREMENTAL_PREFIX = "index_" MANIFEST_PREFIX = "_manifest_" INDEX_EXTENSION = ".idx" -ENCRYPTED_INDEX_EXTENSION = ".enc.idx" # Timestamp format used in filenames (must sort correctly as strings) TIMESTAMP_FORMAT = "%Y%m%dT%H%M%S" @@ -22,68 +18,29 @@ class IndexStorage: """Storage configuration for index operations. - Use the async `create()` classmethod to construct with automatic - encryption detection from existing files. - Example: - storage = await IndexStorage.create(location="/path/to/db") - # storage.is_encrypted is correctly set based on existing files + storage = IndexStorage.create(location="/path/to/db") """ location: str fs: AsyncFilesystem | None = None - is_encrypted: bool = False - encryption_key: str | None = None @classmethod - async def create( + def create( cls, location: str, fs: AsyncFilesystem | None = None, - key: str | None = None, ) -> "IndexStorage": - """Create IndexStorage with encryption status auto-detected. - - Checks index files first, then data files if no index exists. - If encrypted files are detected, validates that the encryption key - is available (either passed directly or from environment). + """Create IndexStorage. Args: location: Path to the database directory. fs: Optional async filesystem for remote storage. - key: Optional encryption key. If not provided and encrypted files - are detected, falls back to SCOUT_DB_ENCRYPTION_KEY env var. Returns: - Configured IndexStorage with is_encrypted set appropriately. - - Raises: - ValueError: If files have mixed encryption status, or if - encrypted files exist but no encryption key is available. + Configured IndexStorage. """ - # Import here to avoid circular imports - from .encryption import ENCRYPTION_KEY_ENV, get_encryption_key_from_env - - storage = cls(location=location, fs=fs, is_encrypted=False) - is_encrypted = await storage._detect_encryption() - - if is_encrypted: - # Use provided key or fall back to environment - resolved_key = key if key is not None else get_encryption_key_from_env() - if not resolved_key: - raise PrerequisiteError( - "Encrypted files detected but no encryption key provided. " - f"Pass key parameter or set {ENCRYPTION_KEY_ENV} environment variable." - ) - else: - resolved_key = None - - return cls( - location=location, - fs=fs, - is_encrypted=is_encrypted, - encryption_key=resolved_key, - ) + return cls(location=location, fs=fs) def is_remote(self) -> bool: """Check if storage location is remote (S3 or HuggingFace).""" @@ -93,47 +50,6 @@ def index_dir_path(self) -> str: """Get path to the _index directory.""" return f"{self.location.rstrip('/')}/{INDEX_DIR}" - def index_extension(self) -> str: - """Get the appropriate index file extension based on encryption status.""" - return ENCRYPTED_INDEX_EXTENSION if self.is_encrypted else INDEX_EXTENSION - - async def _detect_encryption(self) -> bool: - """Detect encryption status from existing files.""" - # Import here to avoid circular imports - from .encryption import ( - _check_data_encryption_status, - _check_index_encryption_status, - ) - from .index import _discover_data_files_internal, _is_index_file - - # First check index files - index_dir = self.index_dir_path() - - if self.is_remote(): - fs = filesystem(self.location) - try: - all_files = fs.ls(index_dir, recursive=False) - idx_files = [f.name for f in all_files if _is_index_file(f.name)] - except FileNotFoundError: - idx_files = [] - else: - index_path = UPath(index_dir) - if index_path.exists(): - idx_files = [str(p) for p in index_path.glob("*" + INDEX_EXTENSION)] - else: - idx_files = [] - - # Check index file encryption status - is_encrypted = _check_index_encryption_status(idx_files) - - # If no index files, check data files - if is_encrypted is None: - data_files = await _discover_data_files_internal(self) - is_encrypted = _check_data_encryption_status(data_files) - - # Default to False if no files exist - return is_encrypted if is_encrypted is not None else False - @dataclass class CompactionResult: diff --git a/src/inspect_scout/_transcript/database/schema.py b/src/inspect_scout/_transcript/database/schema.py index 377af1d05..74607f164 100644 --- a/src/inspect_scout/_transcript/database/schema.py +++ b/src/inspect_scout/_transcript/database/schema.py @@ -37,43 +37,43 @@ class SchemaField: TRANSCRIPT_SCHEMA_FIELDS: list[SchemaField] = [ SchemaField( name="transcript_id", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=True, description="A globally unique identifier for a transcript.", ), SchemaField( name="source_type", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description='Type of transcript source (e.g. "weave", "logfire", "eval_log", etc.). Useful for providing a hint to readers about what might be available in the `metadata` field.', ), SchemaField( name="source_id", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="Globally unique identifier for a transcript source (e.g. a project id).", ), SchemaField( name="source_uri", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="URI for source data (e.g. link to a web page or REST resource for discovering more about the transcript).", ), SchemaField( name="date", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="ISO 8601 datetime of transcript creation.", ), SchemaField( name="task_set", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="Set from which transcript task was drawn (e.g. Inspect task name or benchmark name).", ), SchemaField( name="task_id", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="Identifier for task (e.g. dataset sample id).", ), @@ -85,33 +85,33 @@ class SchemaField: ), SchemaField( name="agent", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="Agent used to execute task.", ), SchemaField( name="agent_args", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="Arguments passed to create agent.", json_serialized=True, ), SchemaField( name="model", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="Main model used by agent.", ), SchemaField( name="model_options", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="Generation options for main model.", json_serialized=True, ), SchemaField( name="score", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="Value indicating score on task.", json_serialized=True, @@ -142,32 +142,51 @@ class SchemaField: ), SchemaField( name="error", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="Error message that terminated the task.", ), SchemaField( name="limit", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description='Limit that caused the task to exit (e.g. "tokens", "messages", etc.).', ), SchemaField( name="messages", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="List of ChatMessage with message history.", json_serialized=True, ), SchemaField( name="events", - pyarrow_type=pa.string(), + pyarrow_type=pa.large_string(), required=False, description="List of Event with event history (e.g. model events, tool events, etc.).", json_serialized=True, ), + SchemaField( + name="timelines", + pyarrow_type=pa.large_string(), + required=False, + description="List of Timeline views over the transcript.", + json_serialized=True, + ), + SchemaField( + name="events_data", + pyarrow_type=pa.large_string(), + required=False, + description="Auxiliary event data: deduplicated message/call pools referenced by ModelEvent refs.", + json_serialized=True, + ), ] +# Large content columns excluded from indexes and lightweight views. +CONTENT_COLUMNS: frozenset[str] = frozenset( + {"messages", "events", "events_data", "timelines"} +) + # --- Public API --- @@ -300,8 +319,8 @@ def validate_transcript_schema( actual_type = schema.field(field.name).type expected_type = field.pyarrow_type - # String columns: allow large_string as equivalent - if expected_type == pa.string(): + # String columns: allow both string and large_string (backward compat) + if pa.types.is_string(expected_type) or pa.types.is_large_string(expected_type): if actual_type not in (pa.string(), pa.large_string()): errors.append( TranscriptSchemaError( @@ -449,6 +468,7 @@ def _pyarrow_to_avro_type(pa_type: pa.DataType, nullable: bool = True) -> Any: """Map PyArrow type to Avro type.""" type_map: dict[pa.DataType, str] = { pa.string(): "string", + pa.large_string(): "string", pa.int64(): "long", pa.int32(): "int", pa.float64(): "double", @@ -465,7 +485,7 @@ def _pyarrow_to_avro_type(pa_type: pa.DataType, nullable: bool = True) -> Any: def _pyarrow_to_json_type(pa_type: pa.DataType) -> str: """Map PyArrow type to JSON Schema type.""" - if pa_type == pa.string(): + if pa_type in (pa.string(), pa.large_string()): return "string" elif pa_type in (pa.int64(), pa.int32()): return "integer" @@ -479,7 +499,7 @@ def _pyarrow_to_json_type(pa_type: pa.DataType) -> str: def _pyarrow_to_pandas_dtype(pa_type: pa.DataType) -> str: """Map PyArrow type to pandas dtype string.""" - if pa_type == pa.string(): + if pa_type in (pa.string(), pa.large_string()): return "object" elif pa_type == pa.int64(): return "Int64" # Nullable integer @@ -499,6 +519,7 @@ def _pyarrow_type_to_display(pa_type: pa.DataType) -> str: """Convert PyArrow type to human-readable display string.""" type_map: dict[pa.DataType, str] = { pa.string(): "string", + pa.large_string(): "string", pa.int64(): "int64", pa.int32(): "int32", pa.float64(): "float64", diff --git a/src/inspect_scout/_transcript/eval_log.py b/src/inspect_scout/_transcript/eval_log.py index 48c095887..b8402c9a4 100644 --- a/src/inspect_scout/_transcript/eval_log.py +++ b/src/inspect_scout/_transcript/eval_log.py @@ -17,6 +17,7 @@ import pandas as pd from inspect_ai._util.asyncfiles import AsyncFilesystem +from inspect_ai._util.zip_common import ZipEntry from inspect_ai.analysis._dataframe.columns import Column from inspect_ai.analysis._dataframe.evals.columns import ( EvalColumn, @@ -54,9 +55,8 @@ from .._query.condition_sql import condition_as_sql, conditions_as_filter from .._scanspec import ScanTranscripts from .._transcript.transcripts import Transcripts -from .._util.async_zip import AsyncZipReader +from .._util.caching_async_zip import CachingAsyncZipReader from .._util.constants import TRANSCRIPT_SOURCE_EVAL_LOG -from .._util.zip_common import ZipEntry from .caching import samples_df_with_caching from .database.database import TranscriptsView from .database.schema import reserved_columns @@ -232,6 +232,10 @@ class EvalLogTranscriptsView(TranscriptsView): connections to the same logs reuse the existing database. """ + @staticmethod + def clear_cache() -> None: + _sqlite_cache.clear() + def __init__( self, logs: Logs | pd.DataFrame, @@ -458,13 +462,29 @@ async def read( f"Reading from {t.source_uri} ({entry.filename})", ): async with await zip_reader.open_member(entry) as json_iterable: - return await load_filtered_transcript( + transcript = await load_filtered_transcript( json_iterable, t, content.messages, content.events, ) + # Fallback: eval logs don't store timelines, so build from events + if ( + content.timeline is not None + and not transcript.timelines + and transcript.events + ): + from inspect_ai.event import timeline_build + + from .util import filter_timelines + + raw_timeline = timeline_build(transcript.events) + timelines = filter_timelines([raw_timeline], content.timeline) + transcript = transcript.model_copy(update={"timelines": timelines}) + + return transcript + @override async def read_messages_events( self, t: TranscriptInfo @@ -484,7 +504,7 @@ async def read_messages_events( if self._files_cache else t.source_uri ) - zip_reader = AsyncZipReader(fs, source_uri) + zip_reader = CachingAsyncZipReader(fs, source_uri) entry = await zip_reader.get_member_entry(sample_filename) inner_cm = await zip_reader.open_member_raw(entry) return TranscriptMessagesAndEvents( @@ -524,7 +544,7 @@ def _get_sample_id_and_epoch(self, t: TranscriptInfo) -> tuple[str, int]: async def _get_zip_reader_and_entry( self, t: TranscriptInfo - ) -> tuple[AsyncZipReader, ZipEntry]: + ) -> tuple[CachingAsyncZipReader, ZipEntry]: """Get ZIP reader and entry for transcript's sample file.""" id_, epoch = self._get_sample_id_and_epoch(t) sample_file_name = f"samples/{id_}_epoch_{epoch}.json" @@ -541,7 +561,7 @@ async def _get_zip_reader_and_entry( if self._files_cache else t.source_uri ) - zip_reader = AsyncZipReader(self._fs, source_uri) + zip_reader = CachingAsyncZipReader(self._fs, source_uri) entry = await zip_reader.get_member_entry(sample_file_name) return zip_reader, entry diff --git a/src/inspect_scout/_transcript/json/load_filtered.py b/src/inspect_scout/_transcript/json/load_filtered.py index a231a07f4..8f9e824e5 100644 --- a/src/inspect_scout/_transcript/json/load_filtered.py +++ b/src/inspect_scout/_transcript/json/load_filtered.py @@ -6,10 +6,10 @@ from typing import IO, Any, Callable import ijson # type: ignore +from inspect_ai._util.async_bytes_reader import AsyncBytesReader, adapt_to_reader +from inspect_ai.event import timeline_load from pydantic import JsonValue -from inspect_scout._util.async_bytes_reader import AsyncBytesReader, adapt_to_reader - from ..types import ( EventFilter, MessageFilter, @@ -18,18 +18,29 @@ TranscriptInfo, ) from ..util import filter_transcript +from .pool import resolve_pools from .reducer import ( ATTACHMENT_PREFIX, ATTACHMENTS_PREFIX, + CALL_POOL_ITEM_PREFIX, EVENTS_ITEM_PREFIX, + MESSAGE_POOL_ITEM_PREFIX, MESSAGES_ITEM_PREFIX, METADATA_PREFIX, + SCORES_PREFIX, + TARGET_PREFIX, + TIMELINES_ITEM_PREFIX, ListProcessingConfig, ParseState, attachments_coroutine, + call_pool_item_coroutine, event_item_coroutine, message_item_coroutine, + message_pool_item_coroutine, metadata_coroutine, + scores_coroutine, + target_coroutine, + timeline_item_coroutine, ) # Pre-compiled regex patterns for performance @@ -41,16 +52,33 @@ _SECTION_EVENTS = 2 _SECTION_ATTACHMENTS = 3 _SECTION_METADATA = 4 +_SECTION_TIMELINES = 5 +_SECTION_TARGET = 6 +_SECTION_SCORES = 7 +_SECTION_MESSAGE_POOL = 8 +_SECTION_CALL_POOL = 9 _MESSAGES_ITEM_PREFIX_LEN = len(MESSAGES_ITEM_PREFIX) _EVENTS_ITEM_PREFIX_LEN = len(EVENTS_ITEM_PREFIX) _ATTACHMENTS_PREFIX_LEN = len(ATTACHMENTS_PREFIX) _METADATA_PREFIX_LEN = len(METADATA_PREFIX) +_TIMELINES_ITEM_PREFIX_LEN = len(TIMELINES_ITEM_PREFIX) +_SCORES_PREFIX_LEN = len(SCORES_PREFIX) +_TARGET_PREFIX_LEN = len(TARGET_PREFIX) +_MESSAGE_POOL_ITEM_PREFIX_LEN = len(MESSAGE_POOL_ITEM_PREFIX) +_CALL_POOL_ITEM_PREFIX_LEN = len(CALL_POOL_ITEM_PREFIX) +# "target" vs "timelines" — discriminate on 2nd char (derived from constant) +_TARGET_CHAR1 = TARGET_PREFIX[1] _MIN_SECTION_PREFIX_LEN = min( _MESSAGES_ITEM_PREFIX_LEN, _EVENTS_ITEM_PREFIX_LEN, _ATTACHMENTS_PREFIX_LEN, _METADATA_PREFIX_LEN, + _TIMELINES_ITEM_PREFIX_LEN, + _SCORES_PREFIX_LEN, + _TARGET_PREFIX_LEN, + _MESSAGE_POOL_ITEM_PREFIX_LEN, + _CALL_POOL_ITEM_PREFIX_LEN, ) @@ -80,6 +108,7 @@ class RawTranscript: metadata: dict[str, Any] messages: list[dict[str, Any]] events: list[dict[str, Any]] + timelines: list[dict[str, Any]] | None = None async def load_filtered_transcript( @@ -87,6 +116,8 @@ async def load_filtered_transcript( t: TranscriptInfo, messages: MessageFilter, events: EventFilter, + *, + on_early_exit: Callable[[], None] | None = None, ) -> Transcript: """ Transform and filter JSON sample data into a Transcript. @@ -104,15 +135,21 @@ async def load_filtered_transcript( list=include matching) events: Filter for event types (None=exclude all, "all"=include all, list=include matching) + on_early_exit: Test-only callback invoked immediately before the + early-exit break Returns: - Transcript object with filtered messages and events, resolved attachments + Transcript object with filtered messages and events, resolved attachments. + Metadata includes sample_metadata, target, and scores from the sample JSON. + ``input`` is not unthinned: the sample JSON's input can contain attachment + refs whose resolution requires parsing the attachments section — which follows + events, defeating the early-exit optimization. """ try: # Phase 1: Parse, filter, and collect attachment references async with adapt_to_reader(sample_bytes) as reader: transcript, attachment_refs = await _parse_and_filter( - reader, t, messages, events + reader, t, messages, events, on_early_exit=on_early_exit ) # Phase 2: Resolve attachment references return _resolve_attachments(transcript, attachment_refs) @@ -136,6 +173,7 @@ async def _load_with_json5_fallback( io_source = sample_bytes io_source.seek(0) data = json.load(io.TextIOWrapper(io_source, encoding="utf-8")) + _resolve_pools_from_dict(data) return filter_transcript( _resolve_attachments( @@ -159,13 +197,10 @@ async def _load_with_json5_fallback( total_tokens=t.total_tokens, error=t.error, limit=t.limit, - metadata=( - t.metadata.copy() | {"sample_metadata": data.get("metadata", {})} - if data.get("metadata") - else t.metadata - ), + metadata=_merge_unthinned_from_dict(t.metadata, data), messages=data.get("messages", []), events=data.get("events", []), + timelines=data.get("timelines"), ), data.get("attachments", {}), ), @@ -173,11 +208,53 @@ async def _load_with_json5_fallback( ) +def _merge_unthinned(base: dict[str, Any], state: ParseState) -> dict[str, Any]: + """Merge unthinned fields from stream parse into transcript metadata.""" + overrides: dict[str, Any] = {} + if state.metadata: + overrides["sample_metadata"] = state.metadata + if state.target is not None: + overrides["target"] = state.target + if state.scores: + overrides["scores"] = state.scores + return base.copy() | overrides if overrides else base + + +def _resolve_pools_from_dict(data: dict[str, Any]) -> None: + """Resolve v3 pools in a fully-parsed sample dict (mutates in-place). + + No-op when pools are absent or empty (v2 files). + """ + from .pool import _resolve_events_pools + + _resolve_events_pools( + data.get("events", []), + data.get("message_pool", []), + data.get("call_pool", []), + ) + + +def _merge_unthinned_from_dict( + base: dict[str, Any], data: dict[str, Any] +) -> dict[str, Any]: + """Merge unthinned fields from fully-parsed dict into transcript metadata.""" + overrides: dict[str, Any] = {} + if data.get("metadata"): + overrides["sample_metadata"] = data["metadata"] + if "target" in data: + overrides["target"] = data["target"] + if data.get("scores"): + overrides["scores"] = data["scores"] + return base.copy() | overrides if overrides else base + + async def _parse_and_filter( sample_json: AsyncBytesReader, t: TranscriptInfo, messages_filter: MessageFilter, events_filter: EventFilter, + *, + on_early_exit: Callable[[], None] | None = None, ) -> tuple[RawTranscript, dict[str, str]]: """ Phase 1: Single-pass stream parse, filter, and collect attachment references. @@ -213,32 +290,47 @@ async def _parse_and_filter( message_item_coroutine(state, messages_config) if messages_config else None ) events_coro = event_item_coroutine(state, events_config) if events_config else None + timelines_coro = timeline_item_coroutine(state) attachments_coro = attachments_coroutine(state) metadata_coro = metadata_coroutine(state) + target_coro = target_coroutine(state) + scores_coro = scores_coroutine(state) + message_pool_coro = message_pool_item_coroutine(state) if events_coro else None + call_pool_coro = call_pool_item_coroutine(state) if events_coro else None last_prefix = "" current_section = _SECTION_OTHER async for prefix, event, value in ijson.parse_async(sample_json, use_float=True): - # Early exit: messages-only with no attachment refs + # Early exit: skip events/attachments when they aren't needed. + # JSON field order is: ...target, messages, output, scores, metadata, + # store, events, attachments — so by the time we see "events" start_array, + # metadata and scores have already been parsed. if ( events_coro is None - and prefix == "messages" - and event == "end_array" + and prefix == "events" + and event == "start_array" and not state.attachment_refs ): + if on_early_exit is not None: + on_early_exit() break - # Inline prefix classification for performance (56M+ calls in hot path) + # Inline prefix classification for performance (56M+ calls in hot path). + # WARNING: every operation here can run millions of times per parse. + # Avoid string slicing, startswith, or any allocation in common paths. + # Profile before changing. if prefix != last_prefix: last_prefix = prefix p_len = len(prefix) - if p_len == 0 or prefix[0] not in ("m", "e", "a"): + if p_len == 0 or prefix[0] not in ("m", "e", "a", "t", "s", "c"): current_section = _SECTION_OTHER elif p_len < _MIN_SECTION_PREFIX_LEN: - # Special case: "metadata" is 8 chars, less than min (9), but valid - if prefix == "metadata": - current_section = _SECTION_METADATA + # Short prefixes: "scores" (6), "target" (6) + if prefix == "scores": + current_section = _SECTION_SCORES + elif prefix == "target": + current_section = _SECTION_TARGET else: current_section = _SECTION_OTHER elif prefix[0] == "m": @@ -254,6 +346,12 @@ async def _parse_and_filter( prefix == "metadata" or prefix.startswith(METADATA_PREFIX) ): current_section = _SECTION_METADATA + elif ( + p_len >= _MESSAGE_POOL_ITEM_PREFIX_LEN + and prefix[:_MESSAGE_POOL_ITEM_PREFIX_LEN] + == MESSAGE_POOL_ITEM_PREFIX + ): + current_section = _SECTION_MESSAGE_POOL else: current_section = _SECTION_OTHER elif ( @@ -268,6 +366,24 @@ async def _parse_and_filter( and prefix[:_ATTACHMENTS_PREFIX_LEN] == ATTACHMENTS_PREFIX ): current_section = _SECTION_ATTACHMENTS + elif ( + prefix[0] == "c" + and p_len >= _CALL_POOL_ITEM_PREFIX_LEN + and prefix[:_CALL_POOL_ITEM_PREFIX_LEN] == CALL_POOL_ITEM_PREFIX + ): + current_section = _SECTION_CALL_POOL + elif prefix[0] == "t": + if prefix[1] == _TARGET_CHAR1: + current_section = _SECTION_TARGET + elif ( + p_len >= _TIMELINES_ITEM_PREFIX_LEN + and prefix[:_TIMELINES_ITEM_PREFIX_LEN] == TIMELINES_ITEM_PREFIX + ): + current_section = _SECTION_TIMELINES + else: + current_section = _SECTION_OTHER + elif prefix[0] == "s" and prefix[:_SCORES_PREFIX_LEN] == SCORES_PREFIX: + current_section = _SECTION_SCORES else: current_section = _SECTION_OTHER @@ -280,6 +396,22 @@ async def _parse_and_filter( attachments_coro.send((prefix, event, value)) elif current_section == _SECTION_METADATA: metadata_coro.send((prefix, event, value)) + elif current_section == _SECTION_TARGET and target_coro is not None: + try: + target_coro.send((prefix, event, value)) + except StopIteration: + target_coro = None + elif current_section == _SECTION_TIMELINES: + timelines_coro.send((prefix, event, value)) + elif current_section == _SECTION_SCORES: + scores_coro.send((prefix, event, value)) + elif current_section == _SECTION_MESSAGE_POOL and message_pool_coro: + message_pool_coro.send((prefix, event, value)) + elif current_section == _SECTION_CALL_POOL and call_pool_coro: + call_pool_coro.send((prefix, event, value)) + + # Resolve v3 pool references before returning + resolve_pools(state) return ( RawTranscript( @@ -302,14 +434,10 @@ async def _parse_and_filter( total_tokens=t.total_tokens, error=t.error, limit=t.limit, - # t.metadata's sample_metadata is potentially thinned, so swap in the full one - metadata=( - t.metadata.copy() | {"sample_metadata": state.metadata} - if state.metadata - else t.metadata - ), + metadata=_merge_unthinned(t.metadata, state), messages=state.messages, events=state.events, + timelines=state.timelines if state.timelines else None, ), state.attachments, ) @@ -358,32 +486,40 @@ def replace_ref(match: re.Match[str]) -> str: # Create new transcript with resolved data # Use model_validate to validate messages/events into proper types, # but pass metadata separately via __pydantic_private__ to preserve LazyJSONDict - validated = Transcript.model_validate( - { - "transcript_id": transcript.id, - "source_type": transcript.source_type, - "source_id": transcript.source_id, - "source_uri": transcript.source_uri, - "date": transcript.date, - "task_set": transcript.task_set, - "task_id": transcript.task_id, - "task_repeat": transcript.task_repeat, - "agent": transcript.agent, - "agent_args": transcript.agent_args, - "model": transcript.model, - "model_options": transcript.model_options, - "score": transcript.score, - "success": transcript.success, - "message_count": transcript.message_count, - "total_time": transcript.total_time, - "total_tokens": transcript.total_tokens, - "error": transcript.error, - "limit": transcript.limit, - "metadata": {}, # Placeholder to avoid validation - "messages": resolved_messages, - "events": resolved_events, - } - ) + transcript_data: dict[str, Any] = { + "transcript_id": transcript.id, + "source_type": transcript.source_type, + "source_id": transcript.source_id, + "source_uri": transcript.source_uri, + "date": transcript.date, + "task_set": transcript.task_set, + "task_id": transcript.task_id, + "task_repeat": transcript.task_repeat, + "agent": transcript.agent, + "agent_args": transcript.agent_args, + "model": transcript.model, + "model_options": transcript.model_options, + "score": transcript.score, + "success": transcript.success, + "message_count": transcript.message_count, + "total_time": transcript.total_time, + "total_tokens": transcript.total_tokens, + "error": transcript.error, + "limit": transcript.limit, + "metadata": {}, # Placeholder to avoid validation + "messages": resolved_messages, + "events": resolved_events, + } + + validated = Transcript.model_validate(transcript_data) + + # Resolve timelines with event UUID context (events must be validated first) + if transcript.timelines: + resolved_timelines = [ + timeline_load(tl_dict, validated.events) for tl_dict in transcript.timelines + ] + validated.timelines = resolved_timelines + # Directly assign metadata to preserve LazyJSONDict validated.metadata = transcript.metadata return validated diff --git a/src/inspect_scout/_transcript/json/pool.py b/src/inspect_scout/_transcript/json/pool.py new file mode 100644 index 000000000..b89c2af86 --- /dev/null +++ b/src/inspect_scout/_transcript/json/pool.py @@ -0,0 +1,55 @@ +"""V3 message/call pool resolution for streaming-parsed eval samples. + +Expands range-encoded pool references (input_refs, call_refs) back into +inline data on raw event dicts. Operates entirely in the dict domain -- +no Pydantic models involved. +""" + +from __future__ import annotations + +from typing import Any + +from .reducer import ParseState + + +def _expand_refs_raw(refs: list[list[int]], pool: list[Any]) -> list[Any]: + """Expand range-encoded refs against a pool. + + Each element is [start, end_exclusive) -- a half-open range yielding + pool[start:end_exclusive]. + """ + result: list[Any] = [] + for start, end_exclusive in refs: + result.extend(pool[start:end_exclusive]) + return result + + +def _resolve_events_pools( + events: list[dict[str, Any]], + message_pool: list[Any], + call_pool: list[Any], +) -> None: + """Expand pool refs in event dicts (mutates in-place). No-op when pools are empty.""" + if not message_pool and not call_pool: + return + for event_dict in events: + input_refs = event_dict.get("input_refs") + if input_refs and message_pool: + event_dict["input"] = _expand_refs_raw(input_refs, message_pool) + event_dict.pop("input_refs", None) + call = event_dict.get("call") + if call and call.get("call_refs") is not None and call_pool: + key = call.get("call_key", "messages") + call.setdefault("request", {})[key] = _expand_refs_raw( + call["call_refs"], call_pool + ) + call.pop("call_refs", None) + call.pop("call_key", None) + + +def resolve_pools(state: ParseState) -> None: + """Expand message_pool/call_pool refs in parsed events (mutates in-place). + + No-op when pools are empty (v2 files). + """ + _resolve_events_pools(state.events, state.message_pool, state.call_pool) diff --git a/src/inspect_scout/_transcript/json/reducer.py b/src/inspect_scout/_transcript/json/reducer.py index d44b0c467..b625ccb63 100644 --- a/src/inspect_scout/_transcript/json/reducer.py +++ b/src/inspect_scout/_transcript/json/reducer.py @@ -12,6 +12,9 @@ ATTACHMENTS_PREFIX = "attachments." MESSAGES_ITEM_PREFIX = "messages.item" EVENTS_ITEM_PREFIX = "events.item" +TIMELINES_ITEM_PREFIX = "timelines.item" +MESSAGE_POOL_ITEM_PREFIX = "message_pool.item" +CALL_POOL_ITEM_PREFIX = "call_pool.item" METADATA_PREFIX = "metadata." @@ -42,9 +45,14 @@ def __post_init__(self) -> None: class ParseState: messages: list[dict[str, Any]] = field(default_factory=list) events: list[dict[str, Any]] = field(default_factory=list) + timelines: list[dict[str, Any]] = field(default_factory=list) attachment_refs: set[str] = field(default_factory=set) attachments: dict[str, str] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict) + target: str | list[str] | None = None + scores: dict[str, Any] = field(default_factory=dict) + message_pool: list[dict[str, Any]] = field(default_factory=list) + call_pool: list[dict[str, Any]] = field(default_factory=list) # --------------------------------------------------------------------------- @@ -123,6 +131,57 @@ def event_item_coroutine( ) +@_ijson_coroutine # type: ignore +def _unfiltered_item_coroutine( + target_list: list[dict[str, Any]], + item_prefix: str, +) -> CoroutineGen: # pragma: no cover + """Collect items from the JSON stream without filtering.""" + builder: ObjectBuilder | None = None + while True: + prefix, event, value = yield + if prefix == item_prefix and event == "start_map": + builder = ObjectBuilder() + builder.event(event, value) + continue + if builder is None: + continue + if prefix == item_prefix and event == "end_map": + try: + builder.event(event, value) + target_list.append(builder.value) + except Exception: + pass + builder = None + continue + try: + builder.event(event, value) + except Exception: + builder = None + continue + + +def timeline_item_coroutine(state: ParseState) -> CoroutineGen: + return cast( + CoroutineGen, + _unfiltered_item_coroutine(state.timelines, TIMELINES_ITEM_PREFIX), + ) + + +def message_pool_item_coroutine(state: ParseState) -> CoroutineGen: + return cast( + CoroutineGen, + _unfiltered_item_coroutine(state.message_pool, MESSAGE_POOL_ITEM_PREFIX), + ) + + +def call_pool_item_coroutine(state: ParseState) -> CoroutineGen: + return cast( + CoroutineGen, + _unfiltered_item_coroutine(state.call_pool, CALL_POOL_ITEM_PREFIX), + ) + + @_ijson_coroutine # type: ignore def attachments_coroutine(state: ParseState) -> CoroutineGen: # pragma: no cover attachments_prefix_len = len(ATTACHMENTS_PREFIX) @@ -172,17 +231,83 @@ def metadata_coroutine(state: ParseState) -> CoroutineGen: # pragma: no cover continue +SCORES_PREFIX = "scores." + + +@_ijson_coroutine # type: ignore +def scores_coroutine(state: ParseState) -> CoroutineGen: # pragma: no cover + """Coroutine to build the scores object from streaming JSON events.""" + builder: ObjectBuilder | None = None + while True: + prefix, event, value = yield + if not (prefix == "scores" or prefix.startswith(SCORES_PREFIX)): + continue + if prefix == "scores" and event == "start_map": + builder = ObjectBuilder() + builder.event(event, value) + continue + if builder is None: + continue + if prefix == "scores" and event == "end_map": + try: + builder.event(event, value) + state.scores = builder.value + except Exception: + pass + builder = None + continue + try: + builder.event(event, value) + except Exception: + builder = None + continue + + +TARGET_PREFIX = "target." + + +@_ijson_coroutine # type: ignore +def target_coroutine(state: ParseState) -> CoroutineGen: # pragma: no cover + """Coroutine to capture the target field (scalar string or list of strings).""" + while True: + prefix, event, value = yield + if prefix != "target" and not prefix.startswith(TARGET_PREFIX): + continue + if prefix == "target" and event == "string": + state.target = value + return + if prefix == "target" and event == "start_array": + items: list[str] = [] + while True: + prefix, event, value = yield + if prefix == "target" and event == "end_array": + state.target = items + return + if prefix == "target.item" and event == "string": + items.append(value) + + __all__ = [ "ListProcessingConfig", "ParseState", "message_item_coroutine", "event_item_coroutine", + "timeline_item_coroutine", + "message_pool_item_coroutine", + "call_pool_item_coroutine", "attachments_coroutine", "metadata_coroutine", + "scores_coroutine", + "target_coroutine", + "SCORES_PREFIX", + "TARGET_PREFIX", "ATTACHMENT_PREFIX", "ATTACHMENT_PREFIX_LEN", "ATTACHMENTS_PREFIX", "MESSAGES_ITEM_PREFIX", "EVENTS_ITEM_PREFIX", + "TIMELINES_ITEM_PREFIX", + "MESSAGE_POOL_ITEM_PREFIX", + "CALL_POOL_ITEM_PREFIX", "METADATA_PREFIX", ] diff --git a/src/inspect_scout/_transcript/messages.py b/src/inspect_scout/_transcript/messages.py new file mode 100644 index 000000000..2cdbffb72 --- /dev/null +++ b/src/inspect_scout/_transcript/messages.py @@ -0,0 +1,471 @@ +"""Message extraction from events, with compaction boundary handling.""" + +from __future__ import annotations + +from collections.abc import AsyncIterator +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal, overload + +from inspect_ai._util._async import tg_collect +from inspect_ai.event import ( + CompactionEvent, + Event, + EventTreeSpan, + ModelEvent, + Timeline, + TimelineEvent, + TimelineSpan, + event_sequence, + event_tree, +) +from inspect_ai.model import ChatMessage, Model, get_model +from inspect_ai.model._chat_message import ChatMessageBase +from inspect_ai.model._model_info import get_model_info + +from inspect_scout._scanner.extract import MessagesAsStr + +if TYPE_CHECKING: + from inspect_scout._transcript.types import Transcript + +DEFAULT_CONTEXT_WINDOW = 128_000 +_BUDGET_DISCOUNT = 0.8 + + +@dataclass(frozen=True) +class MessagesSegment: + """A segment of rendered messages that fits within a token budget. + + Attributes: + messages: The original ChatMessage objects in this segment. + messages_str: Pre-rendered string from messages_as_str. + segment: 0-based segment index, auto-increments across yields. + """ + + messages: list[ChatMessage] + messages_str: str + segment: int + + +async def segment_messages( + source: list[ChatMessage] | list[Event] | TimelineSpan, + *, + messages_as_str: MessagesAsStr, + model: Model | str | None = None, + context_window: int | None = None, + compaction: Literal["all", "last"] | int = "all", +) -> AsyncIterator[MessagesSegment]: + """Render messages and split them into segments that fit within a token budget. + + Renders each message individually via ``messages_as_str``, counts + tokens in parallel via ``tg_collect``, then accumulates segments that + fit within the effective budget (context window * 80%). + + When given events or a ``TimelineSpan``, delegates to + ``span_messages()`` to extract and merge messages (handling + compaction boundaries), then segments the result. + + Args: + source: A list of ChatMessage, a list of Event, or a + TimelineSpan. Events and spans are processed via + ``span_messages()`` first. + messages_as_str: Rendering function from ``message_numbering()``. + Must be called sequentially to preserve counter ordering. + model: Model used for token counting. + context_window: Override for context window size. If None, + looked up via ``get_model_info(model)``. + compaction: How to handle compaction boundaries when source + contains events. Passed through to ``span_messages()``. + + Yields: + MessagesSegment instances, each fitting within the token budget. + Segment counter increments across all yields. + """ + # Resolve model + model = get_model(model) + + # Resolve source to a flat message list + if isinstance(source, TimelineSpan): + messages = span_messages(source, compaction=compaction) + elif source and isinstance(source[0], ChatMessageBase): + messages = list(source) # type: ignore[arg-type] + elif source: + messages = span_messages(source, compaction=compaction) # type: ignore[arg-type] + else: + messages = [] + + if not messages: + return + + # Compute effective budget + if context_window is not None: + budget = context_window + else: + model_info = get_model_info(model) + budget = ( + model_info.input_tokens + if model_info is not None and model_info.input_tokens is not None + else DEFAULT_CONTEXT_WINDOW + ) + effective_budget = int(budget * _BUDGET_DISCOUNT) + + # Pass 1: Render each message sequentially (counter ordering matters) + rendered: list[tuple[ChatMessage, str]] = [] + for msg in messages: + text = await messages_as_str([msg]) + if text: # Skip empty renders (e.g. filtered system messages) + rendered.append((msg, text)) + + if not rendered: + return + + # Pass 2: Count tokens in parallel + token_counts = await tg_collect( + [lambda t=text: model.count_tokens(t) for _, text in rendered] # type: ignore[misc] + ) + + # Pass 3: Segment based on accumulated token counts + segment_counter = 0 + current_messages: list[ChatMessage] = [] + current_texts: list[str] = [] + running_tokens = 0 + + for (msg, text), tokens in zip(rendered, token_counts, strict=False): + if current_messages and running_tokens + tokens > effective_budget: + yield MessagesSegment( + messages=current_messages, + messages_str="\n".join(current_texts), + segment=segment_counter, + ) + segment_counter += 1 + current_messages = [] + current_texts = [] + running_tokens = 0 + + current_messages.append(msg) + current_texts.append(text) + running_tokens += tokens + + # Yield remaining segment + if current_messages: + yield MessagesSegment( + messages=current_messages, + messages_str="\n".join(current_texts), + segment=segment_counter, + ) + + +async def transcript_messages( + transcript: "Transcript", + *, + messages_as_str: MessagesAsStr, + model: Model | str | None = None, + context_window: int | None = None, + compaction: Literal["all", "last"] | int = "all", + depth: int | None = None, + include_scorers: bool = False, +) -> AsyncIterator[MessagesSegment]: + """Yield pre-rendered message segments from a transcript. + + Automatically selects the best extraction strategy based on + what data is available on the transcript: + + - If timelines are present, delegates to ``timeline_messages()`` + - If events are present (no timelines), delegates to + ``segment_messages()`` with compaction handling + - If only messages are present, delegates to ``segment_messages()`` + for context window segmentation only + + By default, scorer events are excluded from extraction. This + applies to both the timeline path (the scorers span is pruned) + and the events path (the scorers section is removed). + + Since ``TimelineMessages`` is structurally compatible with + ``MessagesSegment``, callers get a uniform interface. Those needing + span context can isinstance-check for ``TimelineMessages``. + + Args: + transcript: The transcript to extract messages from. + messages_as_str: Rendering function from ``message_numbering()``. + model: The model used for scanning. + context_window: Override for the model's context window size. + compaction: How to handle compaction boundaries when extracting + messages from events. + depth: Maximum depth of the span tree to process when timelines + are present. Ignored for events-only or messages-only paths. + include_scorers: Whether to include scorer events in message + extraction. Defaults to ``False``. + + Yields: + ``MessagesSegment`` (or ``TimelineMessages``) for each segment. + """ + if transcript.timelines or transcript.events: + from inspect_ai.event import timeline_build, timeline_filter + + from inspect_scout._transcript.timeline import timeline_messages + + # must deal with a timeline for sane message extraction + if transcript.timelines: + timeline = transcript.timelines[0] + else: + timeline = timeline_build( + transcript.events + ) # build a synthetic timeline from events + + if not include_scorers: + timeline = timeline_filter(timeline, lambda s: s.span_type != "scorers") + + async for timeline_seg in timeline_messages( + timeline, + messages_as_str=messages_as_str, + model=model, + context_window=context_window, + compaction=compaction, + depth=depth, + ): + yield timeline_seg # type: ignore[misc] + else: + async for seg in segment_messages( + transcript.messages, + messages_as_str=messages_as_str, + model=model, + context_window=context_window, + ): + yield seg + + +def _exclude_scorers(events: list[Event]) -> list[Event]: + """Remove events belonging to the top-level scorers section. + + Uses ``event_tree()`` to find the scorers span, removes it from the + tree, then flattens back to a sequence with ``event_sequence()``. + """ + tree = event_tree(events) + filtered = [ + item + for item in tree + if not (isinstance(item, EventTreeSpan) and item.name == "scorers") + ] + if len(filtered) == len(tree): + return events # no scorers found, return original + return list(event_sequence(filtered)) + + +@overload +def span_messages( + source: Timeline | TimelineSpan | list[Event], + *, + compaction: Literal["all", "last"] | int = "all", + split_compactions: Literal[False] = False, +) -> list[ChatMessage]: ... + + +@overload +def span_messages( + source: Timeline | TimelineSpan | list[Event], + *, + compaction: Literal["all", "last"] | int = "all", + split_compactions: Literal[True], +) -> list[list[ChatMessage]]: ... + + +def span_messages( + source: Timeline | TimelineSpan | list[Event], + *, + compaction: Literal["all", "last"] | int = "all", + split_compactions: bool = False, +) -> list[ChatMessage] | list[list[ChatMessage]]: + """Extract messages from a span or event list, handling compaction. + + Filters for ``ModelEvent`` and ``CompactionEvent``, then merges + messages into a single list based on the ``compaction`` strategy. + + Args: + source: A ``Timeline`` (extracts ``.root``), ``TimelineSpan`` + (events extracted from its content), or a raw list of events. + Non-Model/Compaction events are ignored. + compaction: How to handle compaction boundaries: + - ``"all"``: merge across boundaries for full coverage. + Summary grafts pre + post messages. Trim prepends the + trimmed prefix. Edit is transparent. + - ``"last"``: ignore compaction history, return only the + last ``ModelEvent``'s input + output. + - ``int``: keep the last *N* compaction regions. ``1`` is + equivalent to ``"last"``. If *N* exceeds the number of + regions the result is the same as ``"all"``. + split_compactions: When ``True``, return one inner list per + compaction region instead of merging into a flat list. + The ``compaction`` parameter still controls how many regions + to keep before splitting. + + Returns: + When ``split_compactions`` is ``False`` (default): merged message + list. When ``True``: one ``list[ChatMessage]`` per kept region. + Empty list if no ``ModelEvent`` is found. + """ + # Normalize Timeline to TimelineSpan + if isinstance(source, Timeline): + source = source.root + + # Extract events from TimelineSpan if needed + if isinstance(source, TimelineSpan): + events = [ + item.event for item in source.content if isinstance(item, TimelineEvent) + ] + else: + events = source + + # Filter to ModelEvents and CompactionEvents + model_events: list[ModelEvent] = [] + for event in events: + if isinstance(event, ModelEvent): + model_events.append(event) + + if not model_events: + return [] + + # Normalize compaction to n: int | None + n: int | None + if compaction == "last": + n = 1 + elif compaction == "all": + n = None + else: + n = compaction + + # "last 1" shortcut: just return the final ModelEvent's messages + if n == 1: + msgs = _segment_messages(model_events[-1]) + return [msgs] if split_compactions else msgs + + # If n is specified, slice events to keep only the last n regions. + # Regions are separated by CompactionEvents. + if n is not None: + compaction_indices = [ + i for i, event in enumerate(events) if isinstance(event, CompactionEvent) + ] + num_regions = len(compaction_indices) + 1 + if n < num_regions: + # Keep the last n regions. The nth-from-last region starts + # right after the compaction event at position -(n) from the + # end of compaction_indices. + cut_index = compaction_indices[-(n)] + events = events[cut_index:] + + # Split mode: return one list per compaction region + if split_compactions: + regions: list[list[ChatMessage]] = [] + current_model_events: list[ModelEvent] = [] + + for event in events: + if isinstance(event, ModelEvent): + current_model_events.append(event) + elif isinstance(event, CompactionEvent): + if current_model_events: + regions.append(_segment_messages(current_model_events[-1])) + current_model_events = [] + + if current_model_events: + regions.append(_segment_messages(current_model_events[-1])) + + return regions + + # Merge across compaction boundaries + merged: list[ChatMessage] = [] + current_model_events_merge: list[ModelEvent] = [] + pending_trim_pre_input: list[ChatMessage] | None = None + + for event in events: + if isinstance(event, ModelEvent): + # If we have a pending trim, compute prefix now that we have + # the first post-compaction ModelEvent + if pending_trim_pre_input is not None: + prefix = _trim_prefix(pending_trim_pre_input, list(event.input)) + merged.extend(prefix) + pending_trim_pre_input = None + + current_model_events_merge.append(event) + + elif isinstance(event, CompactionEvent): + if event.type == "summary": + # Graft pre-compaction messages onto the merged list + if current_model_events_merge: + merged.extend(_segment_messages(current_model_events_merge[-1])) + current_model_events_merge = [] + + elif event.type == "trim": + # Save pre-compaction input for prefix extraction + if current_model_events_merge: + pending_trim_pre_input = list(current_model_events_merge[-1].input) + current_model_events_merge = [] + + # Edit: no action, continue accumulating + + # Append final segment + if current_model_events_merge: + merged.extend(_segment_messages(current_model_events_merge[-1])) + + return merged + + +def _segment_messages(model_event: ModelEvent) -> list[ChatMessage]: + """Extract the conversation from a ModelEvent. + + Returns the model's input messages plus its output message + (the assistant's response). + + Args: + model_event: The ModelEvent to extract messages from. + + Returns: + List of messages: input + output assistant message. + Returns just the input if output is unavailable. + """ + messages = list(model_event.input) + if ( + model_event.output is not None + and model_event.output.choices + and model_event.output.choices[0].message is not None + ): + messages.append(model_event.output.choices[0].message) + return messages + + +def _trim_prefix( + pre_input: list[ChatMessage], + post_input: list[ChatMessage], +) -> list[ChatMessage]: + """Compute the messages trimmed by a trim compaction. + + Finds the overlap point between pre-compaction and post-compaction + inputs, returning the prefix of pre-compaction messages that were + dropped. + + Uses message ``id`` fields for matching. Falls back to content + equality (``message.text``) when IDs don't match. + + Args: + pre_input: Full conversation before trim compaction. + post_input: Trimmed conversation after trim compaction. + + Returns: + Messages from pre_input that were dropped by the trim. + Empty list if no prefix was trimmed or inputs can't be aligned. + """ + if not post_input or not pre_input: + return [] + + # Try matching by message id + first_post_id = post_input[0].id + if first_post_id is not None: + for i, msg in enumerate(pre_input): + if msg.id == first_post_id: + return pre_input[:i] + + # Fall back to content equality: find the first message in pre_input + # whose text matches the first post_input message + first_post_text = post_input[0].text + for i, msg in enumerate(pre_input): + if msg.text == first_post_text and msg.role == post_input[0].role: + return pre_input[:i] + + return [] diff --git a/src/inspect_scout/_transcript/timeline.py b/src/inspect_scout/_transcript/timeline.py new file mode 100644 index 000000000..5d2d360b1 --- /dev/null +++ b/src/inspect_scout/_transcript/timeline.py @@ -0,0 +1,251 @@ +"""Timeline: re-exports from inspect_ai.event and scout-specific utilities. + +Types and builder functions live in ``inspect_ai.event``. This module +re-exports them for backwards compatibility and provides scout-specific +functionality: ``TimelineMessages``, ``timeline_messages``, +``filter_timeline_events``. +""" + +from __future__ import annotations + +from collections.abc import AsyncIterator, Iterator +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal + +if TYPE_CHECKING: + from inspect_scout._scanner.extract import MessagesAsStr + +from inspect_ai.event import ( + ModelEvent, + Timeline, + TimelineBranch, + TimelineEvent, + TimelineSpan, + timeline_build, + timeline_dump, + timeline_filter, + timeline_load, +) +from inspect_ai.event._timeline import ( + Outline, + OutlineNode, + TimelineContentItem, + _timeline_content_discriminator, +) +from inspect_ai.model import ChatMessage, Model + +# Re-export everything that moved to inspect_ai.event +__all__ = [ + # Types + "Outline", + "OutlineNode", + "Timeline", + "TimelineBranch", + "TimelineContentItem", + "TimelineEvent", + "TimelineSpan", + # Functions + "timeline_build", + "timeline_dump", + "timeline_filter", + "timeline_load", + # Scout-specific + "TimelineMessages", + "filter_timeline_events", + "timeline_messages", + # Private helpers (used by other scout modules) + "_timeline_content_discriminator", +] + + +# ============================================================================= +# Timeline Event Filtering (scout-specific) +# ============================================================================= + + +def filter_timeline_events( + timeline: Timeline, + event_types: list[str] | Literal["all"], +) -> Timeline: + """Return a copy of the timeline with only matching event types. + + Walks the tree and removes TimelineEvent nodes whose event.event + is not in event_types. Keeps TimelineSpan structure; prunes empty + spans/branches after filtering. + + Args: + timeline: The timeline to filter. + event_types: Event type strings to keep, or "all" to keep everything. + + Returns: + A new Timeline with only matching events. + """ + if event_types == "all": + return timeline + allowed = set(event_types) + new_root = _filter_span(timeline.root, allowed) + return Timeline(name=timeline.name, description=timeline.description, root=new_root) + + +def _filter_span(span: TimelineSpan, allowed: set[str]) -> TimelineSpan: + """Filter a span's content and branches, keeping only allowed event types.""" + filtered_content = _filter_content_list(span.content, allowed) + filtered_branches = [ + TimelineBranch( + forked_at=b.forked_at, + content=_filter_content_list(b.content, allowed), + ) + for b in span.branches + ] + # Remove branches that ended up empty + filtered_branches = [b for b in filtered_branches if b.content] + return TimelineSpan( + id=span.id, + name=span.name, + span_type=span.span_type, + content=filtered_content, + branches=filtered_branches, + description=span.description, + utility=span.utility, + outline=span.outline, + ) + + +def _filter_content_list( + items: list[TimelineContentItem], + allowed: set[str], +) -> list[TimelineContentItem]: + """Filter content items, keeping events with allowed types and non-empty spans.""" + result: list[TimelineContentItem] = [] + for item in items: + if isinstance(item, TimelineEvent): + if item.event.event in allowed: + result.append(item) + else: # TimelineSpan + filtered = _filter_span(item, allowed) + if filtered.content or filtered.branches: + result.append(filtered) + return result + + +# ============================================================================= +# Timeline Message Extraction +# ============================================================================= + + +@dataclass(frozen=True) +class TimelineMessages: + """A segment of messages from a specific timeline span. + + Structurally compatible with ``MessagesSegment`` (shares + ``messages``, ``messages_str``, ``segment`` fields) with additional + span context. Can be used anywhere a ``MessagesSegment`` + is expected via duck typing. + + Attributes: + messages: The original ChatMessage objects in this segment. + messages_str: Pre-rendered string from messages_as_str. + segment: 0-based segment index, globally unique across yields. + span: The TimelineSpan this segment was extracted from. + """ + + messages: list[ChatMessage] + messages_str: str + segment: int + span: TimelineSpan + + +async def timeline_messages( + timeline: Timeline | TimelineSpan, + *, + messages_as_str: MessagesAsStr, + model: Model | str | None = None, + context_window: int | None = None, + compaction: Literal["all", "last"] | int = "all", + depth: int | None = None, +) -> AsyncIterator[TimelineMessages]: + """Yield pre-rendered message segments from timeline spans. + + Walks the span tree, passes each non-utility span with direct + ``ModelEvent`` content to ``segment_messages()`` for message + extraction and context window segmentation. Each yielded item + includes the span context alongside the pre-rendered text. + + To filter which spans are processed, use ``filter_timeline()`` + before calling this function. + + Args: + timeline: The timeline (or a specific span subtree) to extract + messages from. If a Timeline, starts from timeline.root. + messages_as_str: Rendering function from message_numbering() that + formats messages with globally unique IDs. + model: The model used for scanning. Provides count_tokens() for + measuring rendered text. + context_window: Override for the model's context window size + (in tokens). When None, looked up via get_model_info(). + An 80% discount factor is applied to leave room for system + prompts and scanning overhead. + compaction: How to handle compaction boundaries when extracting + messages from span events. + depth: Maximum depth of the span tree to process. ``1`` processes + only the root span, ``2`` includes immediate children, etc. + None (default) recurses without limit. + + Yields: + TimelineMessages for each segment. Empty spans are skipped. + """ + from inspect_scout._transcript.messages import segment_messages + + root = timeline.root if isinstance(timeline, Timeline) else timeline + + counter = 0 + for span in _walk_spans(root, depth=depth): + async for seg in segment_messages( + span, + messages_as_str=messages_as_str, + model=model, + context_window=context_window, + compaction=compaction, + ): + yield TimelineMessages( + messages=seg.messages, + messages_str=seg.messages_str, + segment=counter, + span=span, + ) + counter += 1 + + +def _walk_spans( + span: TimelineSpan, + *, + depth: int | None = None, + _current_depth: int = 1, +) -> Iterator[TimelineSpan]: + """Walk the span tree depth-first, yielding scannable spans. + + A span is yielded if it is not a utility span and has at least one + direct ``ModelEvent`` in its content. Non-matching spans are still + traversed so their children can be checked. + + Args: + span: The root span to walk. + depth: Maximum depth to recurse. 1 = root only, 2 = root + + children, None = unlimited. + _current_depth: Internal counter tracking current depth. + + Yields: + Scannable TimelineSpan nodes in depth-first order. + """ + if not span.utility and any( + isinstance(item, TimelineEvent) and isinstance(item.event, ModelEvent) + for item in span.content + ): + yield span + + if depth is not None and _current_depth >= depth: + return + + for item in span.content: + if isinstance(item, TimelineSpan): + yield from _walk_spans(item, depth=depth, _current_depth=_current_depth + 1) diff --git a/src/inspect_scout/_transcript/types.py b/src/inspect_scout/_transcript/types.py index eeec43638..392b0085e 100644 --- a/src/inspect_scout/_transcript/types.py +++ b/src/inspect_scout/_transcript/types.py @@ -3,6 +3,8 @@ from os import PathLike from typing import Any, Literal, Protocol, Sequence, TypeAlias +from inspect_ai._util.zip_common import ZipCompressionMethod +from inspect_ai.event import Timeline from inspect_ai.event._event import Event from inspect_ai.log._file import ( EvalLogInfo, @@ -10,8 +12,6 @@ from inspect_ai.model._chat_message import ChatMessage from pydantic import BaseModel, ConfigDict, Field, JsonValue -from .._util.zip_common import ZipCompressionMethod - MessageType = Literal["system", "user", "assistant", "tool"] """Message types.""" @@ -60,6 +60,7 @@ def __init__(self, transcript_id: str, size: int, max_size: int): MessageFilter: TypeAlias = Literal["all"] | Sequence[MessageType] | None EventFilter: TypeAlias = Literal["all"] | Sequence[EventType | str] | None +TimelineFilter: TypeAlias = Literal[True] | Literal["all"] | Sequence[EventType] | None LogPaths: TypeAlias = ( PathLike[str] | str | EvalLogInfo | Sequence[PathLike[str] | str | EvalLogInfo] @@ -68,8 +69,20 @@ def __init__(self, transcript_id: str, size: int, max_size: int): @dataclass class TranscriptContent: + """Content filters for transcript loading. + + Specifies which messages, events, and timeline data to include + when loading transcript content for scanning. + """ + messages: MessageFilter = field(default=None) + """Filter for which message types to include.""" + events: EventFilter = field(default=None) + """Filter for which event types to include.""" + + timeline: TimelineFilter = field(default=None) + """Filter for which timeline events to include.""" class BytesContextManager: @@ -203,3 +216,6 @@ class Transcript(TranscriptInfo): events: list[Event] = Field(default_factory=list) """Events from transcript.""" + + timelines: list[Timeline] = Field(default_factory=list) + """Timeline views over the transcript.""" diff --git a/src/inspect_scout/_transcript/util.py b/src/inspect_scout/_transcript/util.py index 05aec78b9..891ed7351 100644 --- a/src/inspect_scout/_transcript/util.py +++ b/src/inspect_scout/_transcript/util.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import json from functools import reduce -from typing import Any, Iterable, TypeVar +from typing import TYPE_CHECKING, Any, Iterable, TypeVar from inspect_ai.event._event import Event from inspect_ai.model._chat_message import ChatMessage, ChatMessageBase @@ -8,10 +10,14 @@ from .types import ( EventFilter, MessageFilter, + TimelineFilter, Transcript, TranscriptContent, ) +if TYPE_CHECKING: + from inspect_ai.event import Timeline + class LazyJSONDict(dict[str, Any]): """Dictionary that lazily parses JSON strings on first access. @@ -202,7 +208,7 @@ def union_transcript_contents( return reduce( _union_contents, contents, - TranscriptContent(None, None), + TranscriptContent(None, None, None), ) @@ -240,16 +246,19 @@ def filter_transcript(transcript: Transcript, content: TranscriptContent) -> Tra metadata=transcript.metadata, messages=filter_list(transcript.messages, content.messages), events=filter_list(transcript.events, content.events), + timelines=filter_timelines(transcript.timelines, content.timeline), ) def _union_contents(a: TranscriptContent, b: TranscriptContent) -> TranscriptContent: return TranscriptContent( - _union_filters(a.messages, b.messages), _union_filters(a.events, b.events) + _union_filters(a.messages, b.messages), + _union_filters(a.events, b.events), + _union_filters(a.timeline, b.timeline), ) -T = TypeVar("T", MessageFilter, EventFilter) +T = TypeVar("T", MessageFilter, EventFilter, TimelineFilter) def _union_filters(a: T, b: T) -> T: @@ -260,6 +269,9 @@ def _union_filters(a: T, b: T) -> T: if b is None: return a # At this point, both a and b are non-None and non-"all". + # True is normalized before reaching here, but narrow for mypy. + if a is True or b is True: + return True return list(set(a) | set(b)) @@ -281,6 +293,29 @@ def filter_list( ) +def filter_timelines( + timelines: list[Timeline], + filter_value: TimelineFilter, +) -> list[Timeline]: + """Filter timelines by pruning event types. + + Args: + timelines: List of Timeline objects. + filter_value: Timeline filter (None=empty, "all"=all, list=event types to keep). + + Returns: + Filtered list of Timeline objects. + """ + if filter_value is None: + return [] + if filter_value is True or filter_value == "all": + return timelines + # filter_value is a list of event types — prune each timeline + from inspect_scout._transcript.timeline import filter_timeline_events + + return [filter_timeline_events(t, list(filter_value)) for t in timelines] + + def _matches_filter( obj: ChatMessage | Event, filter: MessageFilter | EventFilter ) -> bool: diff --git a/src/inspect_scout/_util/async_bytes_reader.py b/src/inspect_scout/_util/async_bytes_reader.py deleted file mode 100644 index 9b4640ad9..000000000 --- a/src/inspect_scout/_util/async_bytes_reader.py +++ /dev/null @@ -1,121 +0,0 @@ -from collections.abc import AsyncIterable, AsyncIterator -from typing import IO, Protocol, TypeGuard, cast - -import anyio -from typing_extensions import Self - - -class AsyncBytesReader(Protocol): - """Protocol defining the minimal async file-like interface for ijson. - - ijson.parse_async() requires an async file-like object with a read() method that: - - Can be awaited (is an async method) - - Returns bytes (binary mode) - - Accepts a size parameter for the number of bytes to read - - This protocol captures that minimal requirement without requiring the full BinaryIO - interface that includes methods like seek(), tell(), close(), etc. - - Also supports async context manager protocol for usage ensuring proper resource - cleanup. - """ - - async def read(self, size: int) -> bytes: ... - async def aclose(self) -> None: ... - async def __aenter__(self) -> Self: ... - async def __aexit__(self, *args: object) -> None: ... - - -def _is_async_iterable( - io_or_iter: IO[bytes] | AsyncIterable[bytes], -) -> TypeGuard[AsyncIterable[bytes]]: - return hasattr(io_or_iter, "__aiter__") - - -def adapt_to_reader(io_or_iter: IO[bytes] | AsyncIterable[bytes]) -> AsyncBytesReader: - """Adapt a byte source to an async file-like interface (e.g. for ijson). - - Use as async context manager to ensure cleanup of underlying async iterators. - """ - return ( - _BytesIterableReader(io_or_iter) - if _is_async_iterable(io_or_iter) - else _BytesIOReader(cast(IO[bytes], io_or_iter)) - ) - - -class _BytesIOReader(AsyncBytesReader): - """Wrapper to make synchronous I/O operations async-compatible. - - This class is needed because zipfile.ZipFile and other standard library I/O - operations are strictly synchronous. To achieve concurrency and avoid blocking - the main thread, this wrapper uses anyio.to_thread to run blocking I/O operations - in a thread pool while maintaining async/await compatibility. - - The internal lock ensures thread-safe access to the underlying synchronous I/O object. - """ - - def __init__(self, sync_io: IO[bytes]): - self._sync_io = sync_io - self._lock = anyio.Lock() - - async def read(self, size: int) -> bytes: - async with self._lock: - return await anyio.to_thread.run_sync(self._sync_io.read, size) - - async def aclose(self) -> None: - pass # caller owns the IO[bytes] - - async def __aenter__(self) -> Self: - return self - - async def __aexit__(self, *_: object) -> None: - await self.aclose() - - -class _BytesIterableReader(AsyncBytesReader): - """AsyncBytesReader implementation that reads from an AsyncIterable[bytes].""" - - def __init__(self, async_iterable: AsyncIterable[bytes]): - self._async_iter: AsyncIterator[bytes] = aiter(async_iterable) - self._current_chunk: bytes = b"" - self._offset = 0 - - async def read(self, size: int) -> bytes: - if size < 0: - raise ValueError("size must be non-negative") - if size == 0: - return b"" - - chunks_to_return: list[bytes] = [] - total = 0 - - while total < size: - # Get more data from current chunk if available - available = len(self._current_chunk) - self._offset - if available > 0: - bytes_to_take = min(size - total, available) - chunks_to_return.append( - self._current_chunk[self._offset : self._offset + bytes_to_take] - ) - self._offset += bytes_to_take - total += bytes_to_take - else: - # Current chunk exhausted, fetch next - try: - self._current_chunk = await anext(self._async_iter) - self._offset = 0 - except StopAsyncIteration: - break # No more data - - return b"".join(chunks_to_return) - - async def aclose(self) -> None: - if hasattr(self._async_iter, "aclose"): - await self._async_iter.aclose() - - async def __aenter__(self) -> Self: - return self - - async def __aexit__(self, *_: object) -> None: - await self.aclose() diff --git a/src/inspect_scout/_util/async_zip.py b/src/inspect_scout/_util/async_zip.py deleted file mode 100644 index 522fd383a..000000000 --- a/src/inspect_scout/_util/async_zip.py +++ /dev/null @@ -1,392 +0,0 @@ -"""Async ZIP file reader with streaming decompression support. - -Supports reading individual members from large ZIP archives (including ZIP64) -stored locally or remotely (e.g., S3) using async range requests. -""" - -from __future__ import annotations - -import struct -from collections.abc import AsyncIterator -from typing import Any - -import anyio -from inspect_ai._util.asyncfiles import AsyncFilesystem -from typing_extensions import Self - -from .compression_transcoding import CompressedToUncompressedStream -from .zip_common import ZipCompressionMethod, ZipEntry - -# Default chunk size for streaming compressed data (1MB) -DEFAULT_CHUNK_SIZE = 1024 * 1024 - - -# This is an exploratory cache of central directories keyed by filename -# It's not production ready for a variety of reasons. -# The file may have changed since the last read: -# - for some filesystems, we could add the etag into the key -# - we could fall back to modified time?? -# I'm still not confident about the relationship between this class -# and the filesystem class. - -central_directories_cache: dict[str, list[ZipEntry]] = {} -_filename_locks: dict[str, anyio.Lock] = {} -_locks_lock = anyio.Lock() - - -# source_cm was never entered, nothing to close - - -async def _get_central_directory( - filesystem: AsyncFilesystem, filename: str -) -> list[ZipEntry]: - # Fast path: check cache without locks - if (entries := central_directories_cache.get(filename, None)) is not None: - return entries - - # Get or create the lock for this specific filename - async with _locks_lock: - if filename not in _filename_locks: - _filename_locks[filename] = anyio.Lock() - file_lock = _filename_locks[filename] - - # Acquire the per-filename lock - async with file_lock: - # Double-check after acquiring lock - if (entries := central_directories_cache.get(filename, None)) is not None: - return entries - - entries = await _parse_central_directory(filesystem, filename) - central_directories_cache[filename] = entries - return entries - - -async def _find_central_directory( - filesystem: AsyncFilesystem, filename: str -) -> tuple[int, int]: - """Locate and parse the central directory metadata. - - Returns: - Tuple of (cd_offset, cd_size) where cd_offset is the byte offset - of the central directory and cd_size is its size in bytes. - - Raises: - ValueError: If EOCD signature not found or ZIP64 structure is corrupt - """ - size = await filesystem.get_size(filename) - - # Read last 64KB to find EOCD - tail_start = max(0, size - 65536) - tail = await filesystem.read_file_bytes_fully(filename, tail_start, size) - - # Search backward for EOCD signature - eocd_sig = b"PK\x05\x06" - idx = tail.rfind(eocd_sig) - if idx == -1: - raise ValueError("EOCD not found") - - # Parse 32-bit EOCD fields - ( - _disk_no, - _cd_start_disk, - _num_entries_disk, - _num_entries_total, - cd_size_32, - cd_offset_32, - _comment_len, - ) = struct.unpack_from(" list[ZipEntry]: - """Parse the central directory and return all entries. - - Returns: - List of ZipEntry objects, one per member in the archive - """ - cd_offset, cd_size = await _find_central_directory(filesystem, filename) - buf = await filesystem.read_file_bytes_fully( - filename, cd_offset, cd_offset + cd_size - ) - - entries = [] - pos = 0 - sig = b"PK\x01\x02" - - while pos < len(buf): - if pos + 4 > len(buf) or not buf[pos : pos + 4] == sig: - break - - # Parse central directory file header (46 bytes) - ( - _ver_made, - _ver_needed, - _flags, - method, - _time, - _date, - _crc, - compressed_size, - uncompressed_size, - name_len, - extra_len, - comment_len, - _disk, - _int_attr, - _ext_attr, - local_header_off, - ) = struct.unpack_from(" 0: - fields = struct.unpack_from(f"<{num_fields}Q", extra, i) - field_idx = 0 - if uncompressed_size == 0xFFFFFFFF and field_idx < len(fields): - uncompressed_size = fields[field_idx] - field_idx += 1 - if compressed_size == 0xFFFFFFFF and field_idx < len(fields): - compressed_size = fields[field_idx] - field_idx += 1 - if local_header_off == 0xFFFFFFFF and field_idx < len(fields): - local_header_off = fields[field_idx] - break - i += data_size - - entries.append( - ZipEntry( - name, - method, - compressed_size, - uncompressed_size, - local_header_off, - ) - ) - pos += 46 + name_len + extra_len + comment_len - - return entries - - -class _ZipMemberBytes: - """AsyncIterable + AsyncContextManager for zip member data. - - Each iteration creates a fresh decompression stream, enabling re-reads: - - async with await zip_reader.open_member("file.json") as member: - async for chunk in member: # first read - process(chunk) - - async for chunk in member: # second read (e.g., retry on error) - process_differently(chunk) - """ - - def __init__( - self, - filesystem: AsyncFilesystem, - filename: str, - range_and_method: tuple[int, int, ZipCompressionMethod], - *, - raw: bool = False, - ): - self._filesystem = filesystem - self._filename = filename - self._offset, self._end, self._method = range_and_method - self._raw = raw - self._active_streams: set[CompressedToUncompressedStream] = set() - - async def __aiter__(self) -> AsyncIterator[bytes]: - byte_stream = await self._filesystem.read_file_bytes( - self._filename, self._offset, self._end - ) - - if self._raw or self._method == ZipCompressionMethod.STORED: - # Pass through raw bytes directly - no decompression needed - try: - async for chunk in byte_stream: - yield chunk - finally: - await byte_stream.aclose() - else: - # Decompress using the appropriate method - stream = CompressedToUncompressedStream(byte_stream, self._method) - self._active_streams.add(stream) - try: - async for chunk in stream: - yield chunk - finally: - self._active_streams.discard(stream) - await stream.aclose() - - async def __aenter__(self) -> Self: - return self - - async def __aexit__(self, *_args: Any) -> None: - for stream in list(self._active_streams): - await stream.aclose() - self._active_streams.clear() - - -class AsyncZipReader: - """Async ZIP reader that supports streaming decompression of individual members. - - This reader minimizes data transfer by using range requests to read only - the necessary portions of the ZIP file (central directory + requested member). - Supports ZIP64 archives and streams decompressed data incrementally. - - For example: - - async with AsyncFilesystem() as fs: - reader = AsyncZipReader(fs, "s3://bucket/large-archive.zip") - async with await reader.open_member("trajectory_001.json") as iterable: - async for chunk in iterable: - process(chunk) - """ - - def __init__( - self, - filesystem: AsyncFilesystem, - filename: str, - chunk_size: int = DEFAULT_CHUNK_SIZE, - ): - """Initialize the async ZIP reader. - - Args: - filesystem: AsyncFilesystem instance for reading files - filename: Path or URL to ZIP file (local path or s3:// URL) - chunk_size: Size of chunks for streaming compressed data - - Raises: - ValueError: If filename is empty or None - """ - if not filename: - raise ValueError("filename must not be empty") - self._filesystem = filesystem - self._filename = filename - self._chunk_size = chunk_size - self._entries: list[ZipEntry] | None = None - self._entries_lock = anyio.Lock() - - async def get_member_entry(self, member_name: str) -> ZipEntry: - entries = await _get_central_directory(self._filesystem, self._filename) - entry = next((e for e in entries if e.filename == member_name), None) - if entry is None: - raise KeyError(member_name) - return entry - - async def open_member_raw(self, member: str | ZipEntry) -> _ZipMemberBytes: - """Open a ZIP member for streaming its raw (likely compressed) bytes. - - Unlike open_member(), this does NOT decompress the data. Use this when - you want to pass through the raw bytes (e.g., for HTTP streaming with - Content-Encoding: deflate). - - Returns a "cold" iterable - the stream is not opened until iteration. - - Args: - member: Name or ZipEntry of the member file within the archive - - Returns: - _ZipMemberBytes that yields raw bytes (may be compressed) - """ - return _ZipMemberBytes( - self._filesystem, - self._filename, - await self._get_member_range_and_method(member), - raw=True, - ) - - async def open_member(self, member: str | ZipEntry) -> _ZipMemberBytes: - """Open a ZIP member and stream its decompressed contents. - - Must be used as an async context manager to ensure proper cleanup. - Can be re-iterated within the same context manager scope. - - Args: - member: Name or ZipEntry of the member file within the archive - - Returns: - AsyncIterable of decompressed data chunks - - Raises: - KeyError: If member_name not found in archive - NotImplementedError: If compression method is not supported - - Example: - async with await zip_reader.open_member("file.json") as stream: - async for chunk in stream: - process(chunk) - """ - return _ZipMemberBytes( - self._filesystem, - self._filename, - await self._get_member_range_and_method(member), - ) - - async def _get_member_range_and_method( - self, member: str | ZipEntry - ) -> tuple[int, int, ZipCompressionMethod]: - entry = ( - member - if isinstance(member, ZipEntry) - else await self.get_member_entry(member) - ) - - # Read local file header to determine actual data offset - local_header = await self._filesystem.read_file_bytes_fully( - self._filename, - entry.local_header_offset, - entry.local_header_offset + 30, - ) - _, _, _, _, _, _, _, _, _, name_len, extra_len = struct.unpack_from( - "<4sHHHHHIIIHH", local_header - ) - - data_offset = entry.local_header_offset + 30 + name_len + extra_len - data_end = data_offset + entry.compressed_size - return (data_offset, data_end, entry.compression_method) diff --git a/src/inspect_scout/_util/caching_async_zip.py b/src/inspect_scout/_util/caching_async_zip.py new file mode 100644 index 000000000..f10c2b5bb --- /dev/null +++ b/src/inspect_scout/_util/caching_async_zip.py @@ -0,0 +1,37 @@ +"""AsyncZipReader subclass with global central-directory cache.""" + +from __future__ import annotations + +import anyio +from inspect_ai._util.async_zip import AsyncZipReader, CentralDirectory + +_cache: dict[str, CentralDirectory] = {} +_filename_locks: dict[str, anyio.Lock] = {} +_locks_lock = anyio.Lock() + + +class CachingAsyncZipReader(AsyncZipReader): + """AsyncZipReader with process-wide central-directory cache. + + Multiple reader instances hitting the same file share one parsed + central directory, avoiding redundant reads (especially for S3). + """ + + async def entries(self) -> CentralDirectory: + filename = self._filename + if (cd := _cache.get(filename)) is not None: + self._central_directory = cd + return cd + + async with _locks_lock: + if filename not in _filename_locks: + _filename_locks[filename] = anyio.Lock() + lock = _filename_locks[filename] + + async with lock: + if (cd := _cache.get(filename)) is not None: + self._central_directory = cd + return cd + cd = await super().entries() + _cache[filename] = cd + return cd diff --git a/src/inspect_scout/_util/compression.py b/src/inspect_scout/_util/compression.py deleted file mode 100644 index f9b9263ad..000000000 --- a/src/inspect_scout/_util/compression.py +++ /dev/null @@ -1,152 +0,0 @@ -import zlib -from collections.abc import AsyncIterator -from typing import Protocol - -import zstandard - - -class Decompressor(Protocol): - """Protocol for async decompressors that read from a stream iterator.""" - - @property - def exhausted(self) -> bool: - """Whether the decompressor has finished processing all input.""" - ... - - async def decompress_next(self, stream_iterator: AsyncIterator[bytes]) -> bytes: - """Read compressed chunks until decompressed output is available. - - The decompressor may buffer multiple input chunks before producing output, - as it needs to accumulate enough compressed data to decode a full block. - This method keeps reading from the source stream until decompression - yields non-empty output. - - Raises: - StopAsyncIteration: When the stream is exhausted. - """ - ... - - -class ZstdDecompressor(Decompressor): - """Decompressor for zstd compressed data.""" - - def __init__(self) -> None: - self._decompressor: zstandard.ZstdDecompressionObj | None = None - self._exhausted = False - - @property - def exhausted(self) -> bool: - return self._exhausted - - async def decompress_next(self, stream_iterator: AsyncIterator[bytes]) -> bytes: - """Read compressed chunks until decompressed output is available.""" - if self._decompressor is None: - self._decompressor = zstandard.ZstdDecompressor().decompressobj() - while True: - try: - chunk = await stream_iterator.__anext__() - decompressed = self._decompressor.decompress(chunk) - if decompressed: - return decompressed - except StopAsyncIteration: - # Note: Unlike zlib, zstandard's decompressobj doesn't have - # a flush() method. Passing empty bytes can trigger output - # of any remaining buffered data in some edge cases. - try: - final = self._decompressor.decompress(b"") - except zstandard.ZstdError: - final = b"" - self._decompressor = None - self._exhausted = True - if final: - return final - raise - - -class DeflateDecompressor(Decompressor): - """Decompressor for DEFLATE (raw) compressed data.""" - - def __init__(self) -> None: - self._decompressor: zlib._Decompress | None = None - self._exhausted = False - - @property - def exhausted(self) -> bool: - return self._exhausted - - async def decompress_next(self, stream_iterator: AsyncIterator[bytes]) -> bytes: - """Read compressed chunks until decompressed output is available.""" - if self._decompressor is None: - self._decompressor = zlib.decompressobj(-15) # Raw DEFLATE - while True: - try: - chunk = await stream_iterator.__anext__() - decompressed = self._decompressor.decompress(chunk) - if decompressed: - return decompressed - except StopAsyncIteration: - final = self._decompressor.flush() - self._decompressor = None - self._exhausted = True - if final: - return final - raise - - -class Compressor(Protocol): - """Protocol for async compressors that read from a stream iterator.""" - - @property - def exhausted(self) -> bool: - """Whether the compressor has finished processing all input.""" - ... - - async def compress_next(self, stream_iterator: AsyncIterator[bytes]) -> bytes: - """Read uncompressed chunks until compressed output is available. - - The compressor may buffer multiple input chunks before producing output, - as it needs to accumulate enough data to form a compressed block. - This method keeps reading from the source stream until compression - yields non-empty output. - - Raises: - StopAsyncIteration: When the stream is exhausted. - """ - ... - - -class DeflateCompressor(Compressor): - """Compressor for DEFLATE (raw) compressed data.""" - - def __init__(self) -> None: - # wbits=-15 produces raw DEFLATE (no zlib/gzip wrapper) - self._compressor: zlib._Compress | None = zlib.compressobj( - level=6, - wbits=-15, - ) - self._exhausted = False - - @property - def exhausted(self) -> bool: - return self._exhausted - - async def compress_next(self, stream_iterator: AsyncIterator[bytes]) -> bytes: - """Read uncompressed chunks until compressed output is available.""" - while True: - try: - chunk = await stream_iterator.__anext__() - if self._compressor is None: - raise StopAsyncIteration - compressed = self._compressor.compress(chunk) - if compressed: - return compressed - # If no compressed data yet (buffered), continue reading - except StopAsyncIteration: - # Input stream exhausted, flush any remaining data - if self._compressor: - final = self._compressor.flush() - self._compressor = None - self._exhausted = True - if final: - return final - raise diff --git a/src/inspect_scout/_util/compression_transcoding.py b/src/inspect_scout/_util/compression_transcoding.py deleted file mode 100644 index 0a87d44a4..000000000 --- a/src/inspect_scout/_util/compression_transcoding.py +++ /dev/null @@ -1,225 +0,0 @@ -from collections.abc import AsyncIterable, AsyncIterator -from contextlib import AbstractAsyncContextManager -from types import TracebackType -from typing import Literal - -import zstandard -from anyio.abc import ByteReceiveStream - -from .compression import DeflateCompressor, DeflateDecompressor, ZstdDecompressor -from .zip_common import ZipCompressionMethod - - -class CompressedToUncompressedStream(AsyncIterator[bytes]): - """AsyncIterator that decompresses ZIP member data streams. - - Supports DEFLATE (mode 8) and zstd (mode 93) compression methods. - For uncompressed data (COMPRESSION_STORED), use the source stream directly. - - This class provides explicit control over resource cleanup via the aclose() - method, fixing Python 3.12 issues where async generator cleanup could fail - with "generator already running" errors during event loop shutdown. - """ - - def __init__( - self, - compressed_stream: ByteReceiveStream, - compression_method: Literal[ - ZipCompressionMethod.DEFLATE, ZipCompressionMethod.ZSTD - ], - ): - """Initialize the decompression stream. - - Args: - compressed_stream: The compressed input byte stream - compression_method: Compression format of input (8=DEFLATE, 93=zstd). - """ - self._compressed_stream = compressed_stream - self._decompressor = ( - DeflateDecompressor() - if compression_method == ZipCompressionMethod.DEFLATE - else ZstdDecompressor() - ) - self._stream_iterator: AsyncIterator[bytes] | None = None - self._closed = False - - def __aiter__(self) -> AsyncIterator[bytes]: - """Return self as the async iterator.""" - return self - - async def __anext__(self) -> bytes: - if self._closed or self._decompressor.exhausted: - raise StopAsyncIteration - - # Initialize stream iterator on first call - if self._stream_iterator is None: - self._stream_iterator = self._compressed_stream.__aiter__() - - return await self._decompressor.decompress_next(self._stream_iterator) - - async def aclose(self) -> None: - """Explicitly close the stream and underlying resources. - - This method ensures the ByteReceiveStream is properly closed even - when the iterator is not fully consumed. - """ - if self._closed: - return - - self._closed = True - - # Close the underlying stream - await self._compressed_stream.aclose() - - -class CompressedToDeflateStream: - """Async context manager that transcodes a potentially compressed stream to deflate. - - Decompresses the source stream (if compressed) and re-compresses to deflate - for HTTP streaming (browsers support Content-Encoding: deflate but not zstd). - - Example: - async with DeflateTranscodingStream(zstd_cm, COMPRESSION_ZSTD) as stream: - async for chunk in stream: - yield chunk - """ - - def __init__( - self, - source_cm: AbstractAsyncContextManager[AsyncIterable[bytes]], - source_compression: ZipCompressionMethod = ZipCompressionMethod.STORED, - ) -> None: - """Initialize the transcoding stream. - - Args: - source_cm: Async context manager that yields compressed bytes - source_compression: Compression method of source (0=stored, 93=zstd) - """ - self._source_cm = source_cm - self._source_compression = source_compression - self._deflate_stream: _DeflateCompressStream | None = None - self._closed = False - - async def __aenter__(self) -> AsyncIterator[bytes]: - source_iter = await self._source_cm.__aenter__() - try: - # Decompress source if needed, then deflate-compress - if self._source_compression == ZipCompressionMethod.DEFLATE: - # Already deflate-compressed, pass through unchanged - return source_iter.__aiter__() - elif self._source_compression == ZipCompressionMethod.ZSTD: - decompressed_iter = _ZstdDecompressIterator(source_iter.__aiter__()) - self._deflate_stream = _DeflateCompressStream(decompressed_iter) - else: - # Source is uncompressed (COMPRESSION_STORED), just deflate-compress - self._deflate_stream = _DeflateCompressStream(source_iter.__aiter__()) - return self._deflate_stream - except Exception: - # Clean up source if stream creation fails - await self._source_cm.__aexit__(None, None, None) - raise - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - if self._closed: - return - self._closed = True - if self._deflate_stream: - await self._deflate_stream.aclose() - await self._source_cm.__aexit__(exc_type, exc_val, exc_tb) - - async def aclose(self) -> None: - """Explicitly close resources without entering the context manager. - - Safe to call multiple times or after __aexit__. - """ - if self._closed: - return - self._closed = True - # source_cm was never entered, nothing to close - - -class _DeflateCompressStream(AsyncIterator[bytes]): - """AsyncIterator wrapper for deflate-compressing an async byte stream. - - Used to transcode zstd-compressed data to deflate for HTTP streaming, - since browsers support Content-Encoding: deflate but not zstd. - """ - - def __init__(self, source_stream: AsyncIterator[bytes]): - """Initialize the deflate compression stream. - - Args: - source_stream: The input byte stream to compress - """ - self._source_stream = source_stream - self._compressor = DeflateCompressor() - self._closed = False - - def __aiter__(self) -> AsyncIterator[bytes]: - """Return self as the async iterator.""" - return self - - async def __anext__(self) -> bytes: - """Get the next chunk of deflate-compressed data. - - Returns: - Next chunk of compressed bytes - - Raises: - StopAsyncIteration: When stream is exhausted - """ - if self._closed or self._compressor.exhausted: - raise StopAsyncIteration - - return await self._compressor.compress_next(self._source_stream) - - async def aclose(self) -> None: - """Explicitly close the stream. - - Safe to call multiple times. - """ - if self._closed: - return - - self._closed = True - - -class _ZstdDecompressIterator(AsyncIterator[bytes]): - """AsyncIterator that decompresses zstd data from a source iterator.""" - - def __init__(self, source: AsyncIterator[bytes]): - self._source = source - dctx = zstandard.ZstdDecompressor() - self._decompressor: zstandard.ZstdDecompressionObj | None = dctx.decompressobj() - self._exhausted = False - - def __aiter__(self) -> AsyncIterator[bytes]: - return self - - async def __anext__(self) -> bytes: - if self._exhausted or self._decompressor is None: - raise StopAsyncIteration - - while True: - try: - chunk = await self._source.__anext__() - decompressed = self._decompressor.decompress(chunk) - if decompressed: - return decompressed - except StopAsyncIteration: - # Flush any remaining data - if self._decompressor: - try: - final = self._decompressor.decompress(b"") - except zstandard.ZstdError: - final = b"" - self._decompressor = None - self._exhausted = True - if final: - return final - raise diff --git a/src/inspect_scout/_util/zip_common.py b/src/inspect_scout/_util/zip_common.py deleted file mode 100644 index d6b4e4922..000000000 --- a/src/inspect_scout/_util/zip_common.py +++ /dev/null @@ -1,25 +0,0 @@ -from dataclasses import dataclass -from enum import IntEnum - - -class ZipCompressionMethod(IntEnum): - """ZIP compression method constants. - - See: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT - '4.4.5 compression method:' - """ - - STORED = 0 # No compression - DEFLATE = 8 # DEFLATE - ZSTD = 93 # Zstandard - - -@dataclass -class ZipEntry: - """Metadata for a single ZIP archive member.""" - - filename: str - compression_method: ZipCompressionMethod - compressed_size: int - uncompressed_size: int - local_header_offset: int diff --git a/src/inspect_scout/_view/_api_v2_config.py b/src/inspect_scout/_view/_api_v2_config.py index 44a921a9d..9f25db4fe 100644 --- a/src/inspect_scout/_view/_api_v2_config.py +++ b/src/inspect_scout/_view/_api_v2_config.py @@ -11,6 +11,8 @@ ) from upath import UPath +from inspect_scout._transcript.eval_log import EvalLogTranscriptsView + from .._project._project import ( EtagMismatchError, read_project, @@ -48,6 +50,7 @@ def create_config_router( ) async def config(request: Request) -> AppConfig: """Return application configuration.""" + EvalLogTranscriptsView.clear_cache() project = read_project() transcripts_path = view_config.transcripts_cli or project.transcripts scans_path = view_config.scans_cli or project.scans or DEFAULT_SCANS_DIR diff --git a/src/inspect_scout/_view/_api_v2_scans.py b/src/inspect_scout/_view/_api_v2_scans.py index 223b19034..8bb5661a2 100644 --- a/src/inspect_scout/_view/_api_v2_scans.py +++ b/src/inspect_scout/_view/_api_v2_scans.py @@ -2,18 +2,22 @@ import asyncio import io +import json import os import subprocess import sys import tempfile import threading import time +import zipfile from typing import Any, Iterable +import anyio import pyarrow.ipc as pa_ipc from duckdb import InvalidInputException -from fastapi import APIRouter, HTTPException, Path, Response +from fastapi import APIRouter, HTTPException, Path, Request, Response from fastapi.responses import StreamingResponse +from inspect_ai._util.file import file from send2trash import send2trash from starlette.status import ( HTTP_404_NOT_FOUND, @@ -32,6 +36,7 @@ from ._api_v2_types import ( ActiveScansResponse, DistinctRequest, + ScannerInputResponse, ScanRow, ScansRequest, ScansResponse, @@ -45,6 +50,31 @@ _running_scans: set[str] = set() +def _build_scan_zip(scan_path: UPath) -> Response: + """Build a zip archive of all files in a scan directory.""" + if not scan_path.exists(): + raise HTTPException( + status_code=HTTP_404_NOT_FOUND, + detail=f"Scan not found: {scan_path}", + ) + + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + for child in scan_path.iterdir(): + if child.is_file(): + with file(child.as_posix(), "rb") as f: + zf.writestr(child.name, f.read()) + + scan_id = scan_path.name + return Response( + content=buf.getvalue(), + media_type="application/zip", + headers={ + "Content-Disposition": f'attachment; filename="{scan_id}.zip"', + }, + ) + + def create_scans_router( streaming_batch_size: int = 1024, ) -> APIRouter: @@ -177,16 +207,36 @@ async def run_llm_scanner(body: ScanJobConfig) -> ScanStatus: response_model=ScanStatus, response_class=InspectPydanticJSONResponse, summary="Get scan status", - description="Returns detailed status and metadata for a single scan.", + description="Returns detailed status and metadata for a single scan. " + "Send Accept: application/zip to download the scan directory as a zip archive.", + responses={ + 200: { + "content": { + "application/zip": { + "description": "Zip archive of the scan directory " + "when Accept: application/zip is sent.", + }, + }, + } + }, ) async def scan( + request: Request, dir: str = Path(description="Scans directory (base64url-encoded)"), scan: str = Path(description="Scan path (base64url-encoded)"), - ) -> ScanStatus: - """Get detailed status for a single scan.""" + ) -> ScanStatus | Response: + """Get detailed status for a single scan. + + Content negotiation: returns JSON by default, or a zip archive + when the client sends Accept: application/zip. + """ scans_dir = decode_base64url(dir) scan_path = UPath(scans_dir) / decode_base64url(scan) + accept = request.headers.get("accept", "") + if "application/zip" in accept: + return await anyio.to_thread.run_sync(lambda: _build_scan_zip(scan_path)) + try: recorder_status_with_df = await scan_results_df_async( str(scan_path), rows="transcripts" @@ -261,9 +311,10 @@ def stream_as_arrow_ipc() -> Iterable[bytes]: @router.get( "/scans/{dir}/{scan}/{scanner}/{uuid}/input", - summary="Get scanner input for a specific transcript", - description="Returns the original input text for a specific scanner result. " - "The input type is returned in the X-Input-Type response header.", + response_model=ScannerInputResponse, + summary="Get scanner input for a specific result", + description="Returns a JSON envelope with input, input_type, and input_data " + "(EventsData pools for condensed events, or null).", ) async def scanner_input( dir: str = Path(description="Scans directory (base64url-encoded)"), @@ -271,7 +322,12 @@ async def scanner_input( scanner: str = Path(description="Scanner name"), uuid: str = Path(description="UUID of the specific result row"), ) -> Response: - """Retrieve original input text for a scanner result.""" + """Retrieve scanner input as a JSON envelope. + + Returns ``{"input_type": ..., "input": ..., "input_data": ...}`` + where ``input`` and ``input_data`` are raw JSON from parquet — + no server-side parsing or re-encoding. + """ scans_dir = decode_base64url(dir) scan_path = UPath(scans_dir) / decode_base64url(scan) @@ -282,13 +338,29 @@ async def scanner_input( detail=f"Scanner '{scanner}' not found in scan results", ) - input_value = result.get_field(scanner, "uuid", uuid, "input").as_py() - input_type = result.get_field(scanner, "uuid", uuid, "input_type").as_py() + fields = result.get_fields( + scanner, "uuid", uuid, ["input", "input_type", "input_data"] + ) + + # `input` and `input_data` are pre-serialized JSON strings in the parquet + # columns. The call to `.get_fields()` does `.as_py()` which returns a Python + # `str` from Arrow's `large_string`. This means that `fields["input"]` is + # a python `str`. They both pass straight through as raw JSON fragments + # — no parsing, no re-encoding, no extra copies beyond Arrow → Python str. + # Obviously, there's no type safety here — `response_model`` is for OpenAPI + # schema only. return Response( - content=input_value, - media_type="text/plain", - headers={"X-Input-Type": input_type or ""}, + content=( + '{"input_type":' + + json.dumps(fields["input_type"]) + + ',"input":' + + (fields["input"] or "null") + + ',"input_data":' + + (fields["input_data"] or "null") + + "}" + ), + media_type="application/json", ) @router.delete( diff --git a/src/inspect_scout/_view/_api_v2_transcripts.py b/src/inspect_scout/_view/_api_v2_transcripts.py index cbc671973..528d02775 100644 --- a/src/inspect_scout/_view/_api_v2_transcripts.py +++ b/src/inspect_scout/_view/_api_v2_transcripts.py @@ -5,6 +5,8 @@ from fastapi import APIRouter, Header, HTTPException, Path from fastapi.responses import StreamingResponse +from inspect_ai._util.compression_transcoding import CompressedToDeflateStream +from inspect_ai._util.zip_common import ZipCompressionMethod from starlette.status import ( HTTP_404_NOT_FOUND, HTTP_413_CONTENT_TOO_LARGE, @@ -17,8 +19,6 @@ from .._query.order_by import OrderBy from .._transcript.database.factory import transcripts_view from .._transcript.types import TranscriptInfo -from .._util.compression_transcoding import CompressedToDeflateStream -from .._util.zip_common import ZipCompressionMethod from ._api_v2_types import ( DistinctRequest, MessagesEventsResponse, diff --git a/src/inspect_scout/_view/_api_v2_types.py b/src/inspect_scout/_view/_api_v2_types.py index 8169a1029..5fcb9d5e3 100644 --- a/src/inspect_scout/_view/_api_v2_types.py +++ b/src/inspect_scout/_view/_api_v2_types.py @@ -1,7 +1,10 @@ from dataclasses import dataclass, field from typing import Any, Literal, TypeAlias +from inspect_ai._util.zip_common import ZipCompressionMethod +from inspect_ai.event import Timeline from inspect_ai.event._event import Event +from inspect_ai.log import EventsData from inspect_ai.model._chat_message import ChatMessage from pydantic import BaseModel, ConfigDict, JsonValue @@ -12,9 +15,9 @@ from .._recorder.recorder import Status as RecorderStatus from .._recorder.summary import Summary from .._scanner.result import Error +from .._scanner.types import ScannerInput, ScannerInputNames from .._scanspec import ScanSpec from .._transcript.types import TranscriptInfo -from .._util.zip_common import ZipCompressionMethod @dataclass @@ -262,12 +265,27 @@ class ScannersResponse: items: list[ScannerInfo] +class ScannerInputResponse(BaseModel): + """Response body for GET /scans/{dir}/{scan}/scanners/{scanner}/input/{uuid}. + + Used only for OpenAPI schema generation — the endpoint returns a + pre-serialized JSON string via ``Response`` to avoid parsing/re-encoding + the raw parquet payloads. + """ + + input_type: ScannerInputNames + input: ScannerInput + input_data: EventsData | None = None + + class MessagesEventsResponse(BaseModel): """Response for GET /transcripts/{dir}/{id}/messages-events endpoint.""" messages: list[ChatMessage] events: list[Event] + timelines: list[Timeline] attachments: dict[str, str] | None = None + events_data: EventsData | None = None model_config = ConfigDict(extra="allow") diff --git a/src/inspect_scout/_view/_openapi.py b/src/inspect_scout/_view/_openapi.py index 79d4870b5..7ac62d9e8 100644 --- a/src/inspect_scout/_view/_openapi.py +++ b/src/inspect_scout/_view/_openapi.py @@ -1,8 +1,9 @@ """OpenAPI schema generation helpers.""" -from typing import Any, Literal, Union, get_args, get_origin +from typing import Any, Literal, Union, get_args, get_origin, is_typeddict from fastapi import FastAPI +from pydantic import TypeAdapter from ._server_common import CustomJsonSchemaGenerator @@ -64,6 +65,16 @@ def build_openapi_schema( elif get_origin(t) is Literal: # Literal type: create enum schema schemas[name] = {"type": "string", "enum": list(get_args(t))} + elif is_typeddict(t): + # TypedDict: generate schema via TypeAdapter (no CustomJsonSchemaGenerator). + # Only merge $defs that don't already exist to avoid clobbering schemas + # generated with CustomJsonSchemaGenerator (which treats defaults-with- + # required differently). + ta = TypeAdapter(t) + schema = ta.json_schema(ref_template=ref_template) + for def_name, def_schema in schema.get("$defs", {}).items(): + schemas.setdefault(def_name, def_schema) + schemas[name] = _strip_defaults_from_nullable(_schema_without_defs(schema)) elif hasattr(t, "model_json_schema"): # Pydantic model: add directly schema = t.model_json_schema( diff --git a/src/inspect_scout/_view/dist/assets/_commonjsHelpers-Cpj98o6Y.js b/src/inspect_scout/_view/dist/assets/_commonjsHelpers-Cpj98o6Y.js new file mode 100644 index 000000000..93aa73ec4 --- /dev/null +++ b/src/inspect_scout/_view/dist/assets/_commonjsHelpers-Cpj98o6Y.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e898f2560233fe672543bbaffe66542d387208b18f5639cb3050bd75d167e48 +size 290 diff --git a/src/inspect_scout/_view/dist/assets/chunk-DfAF0w94-qwHW_6P2.js b/src/inspect_scout/_view/dist/assets/chunk-DfAF0w94-qwHW_6P2.js new file mode 100644 index 000000000..e65c0662d --- /dev/null +++ b/src/inspect_scout/_view/dist/assets/chunk-DfAF0w94-qwHW_6P2.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ca34c6c2ed0af6c407e49037a7c032895440ec675c247ad75a307055c43ce96 +size 609 diff --git a/src/inspect_scout/_view/dist/assets/favicon-CLlGywfF.svg b/src/inspect_scout/_view/dist/assets/favicon-CLlGywfF.svg new file mode 100644 index 000000000..7cd1ec764 --- /dev/null +++ b/src/inspect_scout/_view/dist/assets/favicon-CLlGywfF.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4b118c3b67e1af8c1c2b271e880e3014bbc30ca8d8b5790a6dc762b517b6514 +size 1240 diff --git a/src/inspect_scout/_view/dist/assets/index-CuHOvLd4.js b/src/inspect_scout/_view/dist/assets/index-CuHOvLd4.js new file mode 100644 index 000000000..567a594ef --- /dev/null +++ b/src/inspect_scout/_view/dist/assets/index-CuHOvLd4.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8608e5ef7ac936133fcb8096a796a4a0abef0c8edeb6fc3d545254eda406b1d +size 3127751 diff --git a/src/inspect_scout/_view/dist/assets/index-D-8nvD2T.css b/src/inspect_scout/_view/dist/assets/index-D-8nvD2T.css new file mode 100644 index 000000000..655a4fb20 --- /dev/null +++ b/src/inspect_scout/_view/dist/assets/index-D-8nvD2T.css @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3ebddeba47d16a74159556e763c79bd4f19251888462061201f4cfe7857b331 +size 890305 diff --git a/src/inspect_scout/_view/dist/assets/lib-CBtriEt5-BElwhvdn.js b/src/inspect_scout/_view/dist/assets/lib-CBtriEt5-BElwhvdn.js new file mode 100644 index 000000000..e004ebb9e --- /dev/null +++ b/src/inspect_scout/_view/dist/assets/lib-CBtriEt5-BElwhvdn.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d953141b71a5492ac5c133ebe748c784d9483d056ac22aeb2d7e9a780cd0ccff +size 60127 diff --git a/src/inspect_scout/_view/dist/assets/liteDOM-Cp0aN3bP-CB7p9OS9.js b/src/inspect_scout/_view/dist/assets/liteDOM-Cp0aN3bP-CB7p9OS9.js new file mode 100644 index 000000000..505ae6be1 --- /dev/null +++ b/src/inspect_scout/_view/dist/assets/liteDOM-Cp0aN3bP-CB7p9OS9.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37089a5948d5236f7ba60fb493cf9fb37b29381357218857fa3f24682862c039 +size 23010 diff --git a/src/inspect_scout/_view/dist/assets/tex-svg-full-BI3fonbT-HSI3S7k5.js b/src/inspect_scout/_view/dist/assets/tex-svg-full-BI3fonbT-HSI3S7k5.js new file mode 100644 index 000000000..07f5dcec7 --- /dev/null +++ b/src/inspect_scout/_view/dist/assets/tex-svg-full-BI3fonbT-HSI3S7k5.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:444a05d6b685570e5c362bd87d44ea556683d513cc070a165dbb2224129eaacb +size 2235312 diff --git a/src/inspect_scout/_view/dist/assets/wgxpath.install-node-Csk64Aj9-BNsAtpe9.js b/src/inspect_scout/_view/dist/assets/wgxpath.install-node-Csk64Aj9-BNsAtpe9.js new file mode 100644 index 000000000..346a1ad7e --- /dev/null +++ b/src/inspect_scout/_view/dist/assets/wgxpath.install-node-Csk64Aj9-BNsAtpe9.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:359841c1d88f319c58f25cf0e6ed01e4c78349e5d1fedebfb9c8ac83d4cc68b5 +size 28048 diff --git a/src/inspect_scout/_view/dist/assets/xypic-DrMJn58R-DWcEM0NB.js b/src/inspect_scout/_view/dist/assets/xypic-DrMJn58R-DWcEM0NB.js new file mode 100644 index 000000000..d9d36fc1b --- /dev/null +++ b/src/inspect_scout/_view/dist/assets/xypic-DrMJn58R-DWcEM0NB.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15bce35a9c45f59faa9526737b9b1da555432cb4cfb031fa658dbfc429ba4130 +size 352173 diff --git a/src/inspect_scout/_view/dist/index.html b/src/inspect_scout/_view/dist/index.html new file mode 100644 index 000000000..12d43a779 --- /dev/null +++ b/src/inspect_scout/_view/dist/index.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b93d00156b7f0894edd6a64b66a958ded44c8f13ba37c1018607b2a691091608 +size 1217 diff --git a/src/inspect_scout/_view/invalidationTopics.py b/src/inspect_scout/_view/invalidationTopics.py index fc2b920d3..f7e136ee6 100644 --- a/src/inspect_scout/_view/invalidationTopics.py +++ b/src/inspect_scout/_view/invalidationTopics.py @@ -6,7 +6,7 @@ import anyio -InvalidationTopic: TypeAlias = Literal["project-config", "scans"] +InvalidationTopic: TypeAlias = Literal["project-config", "scans", "transcripts"] _startup_timestamp = datetime.now(timezone.utc).isoformat() _versions: dict[InvalidationTopic, str] = { diff --git a/src/inspect_scout/_view/notify.py b/src/inspect_scout/_view/notify.py index 4e014eb72..760b72baf 100644 --- a/src/inspect_scout/_view/notify.py +++ b/src/inspect_scout/_view/notify.py @@ -1,17 +1,63 @@ import json import os +from contextlib import asynccontextmanager from pathlib import Path +from typing import AsyncIterator from urllib.parse import urlparse +import anyio +from fastapi import FastAPI from inspect_ai._util.vscode import vscode_workspace_id +from inspect_ai._view.notify import view_last_eval_time +from inspect_scout._transcript.eval_log import EvalLogTranscriptsView from inspect_scout._util.appdirs import scout_data_dir +from inspect_scout._view.invalidationTopics import InvalidationTopic, notify_topics # lightweight tracking of when the last scan completed # this enables the scout client to poll for changes frequently # (e.g. every 1 second) with very minimal overhead. +@asynccontextmanager +async def notify_lifespan(_app: FastAPI) -> AsyncIterator[None]: + file_times: dict[str, int] = { + "last_scan": view_last_scan_time(), + "last_eval": view_last_eval_time(), + } + + async def notify_worker() -> None: + while True: + # sleep between checks + await anyio.sleep(5) + + # invalidation topics + invalidations: list[InvalidationTopic] = [] + + # clear eval cache if an eval has completed recently + last_eval_time = view_last_eval_time() + if last_eval_time > file_times["last_eval"]: + file_times["last_eval"] = last_eval_time + EvalLogTranscriptsView.clear_cache() + invalidations.append("transcripts") + + # clear scan cache if a scan has completed recently + last_scan_time = view_last_scan_time() + if last_scan_time > file_times["last_scan"]: + file_times["last_scan"] = last_scan_time + invalidations.append("scans") + + if len(invalidations): + await notify_topics(invalidations) + + async with anyio.create_task_group() as tg: + tg.start_soon(notify_worker) + try: + yield + finally: + tg.cancel_scope.cancel() + + def view_notify_scan(location: str) -> None: # do not do this when running under pytest if os.environ.get("PYTEST_VERSION", None) is not None: diff --git a/src/inspect_scout/_view/www/openapi.json b/src/inspect_scout/_view/openapi.json similarity index 93% rename from src/inspect_scout/_view/www/openapi.json rename to src/inspect_scout/_view/openapi.json index a0e2d91e2..882f1b513 100644 --- a/src/inspect_scout/_view/www/openapi.json +++ b/src/inspect_scout/_view/openapi.json @@ -1661,7 +1661,8 @@ "enum": [ "auto", "low", - "high" + "high", + "original" ], "title": "Detail", "type": "string" @@ -2304,6 +2305,45 @@ } ] }, + "EventsData": { + "additionalProperties": true, + "description": "Pooled data extracted by condense_events / condense_sample.", + "properties": { + "calls": { + "items": { + "$ref": "#/components/schemas/JsonValue" + }, + "title": "Calls", + "type": "array" + }, + "messages": { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ChatMessageSystem" + }, + { + "$ref": "#/components/schemas/ChatMessageUser" + }, + { + "$ref": "#/components/schemas/ChatMessageAssistant" + }, + { + "$ref": "#/components/schemas/ChatMessageTool" + } + ] + }, + "title": "Messages", + "type": "array" + } + }, + "required": [ + "messages", + "calls" + ], + "title": "EventsData", + "type": "object" + }, "GenerateConfig": { "description": "Model generation options.", "properties": { @@ -2525,6 +2565,29 @@ "default": null, "title": "Max Tool Output" }, + "modalities": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "const": "image", + "type": "string" + }, + { + "$ref": "#/components/schemas/ImageOutput" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modalities" + }, "num_choices": { "anyOf": [ { @@ -2965,6 +3028,28 @@ ], "title": "Max Tool Output" }, + "modalities": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "const": "image", + "type": "string" + }, + { + "$ref": "#/components/schemas/ImageOutput" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Modalities" + }, "num_choices": { "anyOf": [ { @@ -3388,6 +3473,28 @@ ], "title": "Max Tool Output" }, + "modalities": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "const": "image", + "type": "string" + }, + { + "$ref": "#/components/schemas/ImageOutput" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Modalities" + }, "num_choices": { "anyOf": [ { @@ -3619,6 +3726,32 @@ "title": "HTTPValidationError", "type": "object" }, + "ImageOutput": { + "description": "Image output configuration.\n\nUse the `options` field to pass provider-specific options directly\nto the underlying API (e.g. OpenAI image_generation tool parameters).", + "properties": { + "options": { + "anyOf": [ + { + "additionalProperties": { + "additionalProperties": true, + "type": "object" + }, + "propertyNames": { + "const": "openai" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Options" + } + }, + "title": "ImageOutput", + "type": "object" + }, "InfoEvent": { "description": "Event with custom info/data.", "properties": { @@ -3800,7 +3933,8 @@ "InvalidationTopic": { "enum": [ "project-config", - "scans" + "scans", + "transcripts" ], "type": "string" }, @@ -4697,6 +4831,16 @@ "title": "Events", "type": "array" }, + "events_data": { + "anyOf": [ + { + "$ref": "#/components/schemas/EventsData" + }, + { + "type": "null" + } + ] + }, "messages": { "items": { "anyOf": [ @@ -4716,11 +4860,19 @@ }, "title": "Messages", "type": "array" + }, + "timelines": { + "items": { + "$ref": "#/components/schemas/Timeline" + }, + "title": "Timelines", + "type": "array" } }, "required": [ "messages", - "events" + "events", + "timelines" ], "title": "MessagesEventsResponse", "type": "object" @@ -4728,6 +4880,55 @@ "ModelCall": { "description": "Model call (raw request/response data).", "properties": { + "call_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Call Key" + }, + "call_refs": { + "anyOf": [ + { + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ], + "type": "array" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Call Refs" + }, + "error": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error" + }, "request": { "additionalProperties": { "$ref": "#/components/schemas/JsonValue" @@ -4923,6 +5124,31 @@ "title": "Input", "type": "array" }, + "input_refs": { + "anyOf": [ + { + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ], + "type": "array" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Input Refs" + }, "metadata": { "anyOf": [ { @@ -5283,6 +5509,45 @@ "title": "OrderBy", "type": "object" }, + "Outline": { + "description": "Hierarchical outline of events for an agent.", + "properties": { + "nodes": { + "items": { + "$ref": "#/components/schemas/OutlineNode" + }, + "title": "Nodes", + "type": "array" + } + }, + "required": [ + "nodes" + ], + "title": "Outline", + "type": "object" + }, + "OutlineNode": { + "description": "A node in an agent's outline, referencing an event by UUID.", + "properties": { + "children": { + "items": { + "$ref": "#/components/schemas/OutlineNode" + }, + "title": "Children", + "type": "array" + }, + "event": { + "title": "Event", + "type": "string" + } + }, + "required": [ + "event", + "children" + ], + "title": "OutlineNode", + "type": "object" + }, "Pagination": { "properties": { "cursor": { @@ -7492,54 +7757,257 @@ "title": "ScannerInfo", "type": "object" }, - "ScannerParam": { + "ScannerInputResponse": { + "description": "Response body for GET /scans/{dir}/{scan}/scanners/{scanner}/input/{uuid}.\n\nUsed only for OpenAPI schema generation \u2014 the endpoint returns a\npre-serialized JSON string via ``Response`` to avoid parsing/re-encoding\nthe raw parquet payloads.", "properties": { - "default": { + "input": { "anyOf": [ - {}, { - "type": "null" - } - ], - "title": "Default" - }, - "name": { - "title": "Name", - "type": "string" - }, - "required": { - "title": "Required", - "type": "boolean" - }, - "schema": { - "additionalProperties": true, - "title": "Schema", - "type": "object" - } - }, - "required": [ - "name", - "schema", - "required" - ], - "title": "ScannerParam", - "type": "object" - }, - "ScannerSpec": { - "description": "Scanner used by scan.", - "properties": { - "file": { - "anyOf": [ + "$ref": "#/components/schemas/Transcript" + }, { - "type": "string" + "$ref": "#/components/schemas/ChatMessageSystem" }, { - "type": "null" - } - ], - "title": "File" - }, - "name": { + "$ref": "#/components/schemas/ChatMessageUser" + }, + { + "$ref": "#/components/schemas/ChatMessageAssistant" + }, + { + "$ref": "#/components/schemas/ChatMessageTool" + }, + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ChatMessageSystem" + }, + { + "$ref": "#/components/schemas/ChatMessageUser" + }, + { + "$ref": "#/components/schemas/ChatMessageAssistant" + }, + { + "$ref": "#/components/schemas/ChatMessageTool" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/components/schemas/SampleInitEvent" + }, + { + "$ref": "#/components/schemas/SampleLimitEvent" + }, + { + "$ref": "#/components/schemas/SandboxEvent" + }, + { + "$ref": "#/components/schemas/StateEvent" + }, + { + "$ref": "#/components/schemas/StoreEvent" + }, + { + "$ref": "#/components/schemas/ModelEvent" + }, + { + "$ref": "#/components/schemas/ToolEvent" + }, + { + "$ref": "#/components/schemas/ApprovalEvent" + }, + { + "$ref": "#/components/schemas/CompactionEvent" + }, + { + "$ref": "#/components/schemas/InputEvent" + }, + { + "$ref": "#/components/schemas/ScoreEvent" + }, + { + "$ref": "#/components/schemas/ScoreEditEvent" + }, + { + "$ref": "#/components/schemas/ErrorEvent" + }, + { + "$ref": "#/components/schemas/LoggerEvent" + }, + { + "$ref": "#/components/schemas/InfoEvent" + }, + { + "$ref": "#/components/schemas/SpanBeginEvent" + }, + { + "$ref": "#/components/schemas/SpanEndEvent" + }, + { + "$ref": "#/components/schemas/StepEvent" + }, + { + "$ref": "#/components/schemas/SubtaskEvent" + }, + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/SampleInitEvent" + }, + { + "$ref": "#/components/schemas/SampleLimitEvent" + }, + { + "$ref": "#/components/schemas/SandboxEvent" + }, + { + "$ref": "#/components/schemas/StateEvent" + }, + { + "$ref": "#/components/schemas/StoreEvent" + }, + { + "$ref": "#/components/schemas/ModelEvent" + }, + { + "$ref": "#/components/schemas/ToolEvent" + }, + { + "$ref": "#/components/schemas/ApprovalEvent" + }, + { + "$ref": "#/components/schemas/CompactionEvent" + }, + { + "$ref": "#/components/schemas/InputEvent" + }, + { + "$ref": "#/components/schemas/ScoreEvent" + }, + { + "$ref": "#/components/schemas/ScoreEditEvent" + }, + { + "$ref": "#/components/schemas/ErrorEvent" + }, + { + "$ref": "#/components/schemas/LoggerEvent" + }, + { + "$ref": "#/components/schemas/InfoEvent" + }, + { + "$ref": "#/components/schemas/SpanBeginEvent" + }, + { + "$ref": "#/components/schemas/SpanEndEvent" + }, + { + "$ref": "#/components/schemas/StepEvent" + }, + { + "$ref": "#/components/schemas/SubtaskEvent" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/components/schemas/Timeline" + }, + { + "items": { + "$ref": "#/components/schemas/Timeline" + }, + "type": "array" + } + ], + "title": "Input" + }, + "input_data": { + "anyOf": [ + { + "$ref": "#/components/schemas/EventsData" + }, + { + "type": "null" + } + ] + }, + "input_type": { + "enum": [ + "transcript", + "event", + "events", + "message", + "messages", + "timeline", + "timelines" + ], + "title": "Input Type", + "type": "string" + } + }, + "required": [ + "input_type", + "input" + ], + "title": "ScannerInputResponse", + "type": "object" + }, + "ScannerParam": { + "properties": { + "default": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "title": "Default" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "title": "Required", + "type": "boolean" + }, + "schema": { + "additionalProperties": true, + "title": "Schema", + "type": "object" + } + }, + "required": [ + "name", + "schema", + "required" + ], + "title": "ScannerParam", + "type": "object" + }, + "ScannerSpec": { + "description": "Scanner used by scan.", + "properties": { + "file": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File" + }, + "name": { "title": "Name", "type": "string" }, @@ -8117,6 +8585,21 @@ "default": null, "title": "Pending" }, + "role_usage": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/ModelUsage" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Role Usage" + }, "score": { "$ref": "#/components/schemas/Score" }, @@ -8835,6 +9318,245 @@ "title": "Summary", "type": "object" }, + "Timeline": { + "description": "A named timeline view over a transcript.\n\nMultiple timelines allow different interpretations of the same event\nstream \u2014 e.g. a default agent-centric view alongside an alternative\ngrouping or filtered view.", + "properties": { + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "root": { + "$ref": "#/components/schemas/TimelineSpan" + } + }, + "required": [ + "name", + "description", + "root" + ], + "title": "Timeline", + "type": "object" + }, + "TimelineBranch": { + "description": "A discarded alternative path from a branch point.", + "properties": { + "content": { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TimelineEvent" + }, + { + "$ref": "#/components/schemas/TimelineSpan" + } + ] + }, + "title": "Content", + "type": "array" + }, + "forked_at": { + "title": "Forked At", + "type": "string" + }, + "type": { + "const": "branch", + "default": "branch", + "title": "Type", + "type": "string" + } + }, + "required": [ + "type", + "forked_at", + "content" + ], + "title": "TimelineBranch", + "type": "object" + }, + "TimelineEvent": { + "description": "Wraps a single Event.", + "properties": { + "event": { + "anyOf": [ + { + "$ref": "#/components/schemas/SampleInitEvent" + }, + { + "$ref": "#/components/schemas/SampleLimitEvent" + }, + { + "$ref": "#/components/schemas/SandboxEvent" + }, + { + "$ref": "#/components/schemas/StateEvent" + }, + { + "$ref": "#/components/schemas/StoreEvent" + }, + { + "$ref": "#/components/schemas/ModelEvent" + }, + { + "$ref": "#/components/schemas/ToolEvent" + }, + { + "$ref": "#/components/schemas/ApprovalEvent" + }, + { + "$ref": "#/components/schemas/CompactionEvent" + }, + { + "$ref": "#/components/schemas/InputEvent" + }, + { + "$ref": "#/components/schemas/ScoreEvent" + }, + { + "$ref": "#/components/schemas/ScoreEditEvent" + }, + { + "$ref": "#/components/schemas/ErrorEvent" + }, + { + "$ref": "#/components/schemas/LoggerEvent" + }, + { + "$ref": "#/components/schemas/InfoEvent" + }, + { + "$ref": "#/components/schemas/SpanBeginEvent" + }, + { + "$ref": "#/components/schemas/SpanEndEvent" + }, + { + "$ref": "#/components/schemas/StepEvent" + }, + { + "$ref": "#/components/schemas/SubtaskEvent" + } + ], + "title": "Event" + }, + "type": { + "const": "event", + "default": "event", + "title": "Type", + "type": "string" + } + }, + "required": [ + "type", + "event" + ], + "title": "TimelineEvent", + "type": "object" + }, + "TimelineSpan": { + "description": "A span of execution \u2014 agent, scorer, tool, or root.", + "properties": { + "agent_result": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Agent Result" + }, + "branches": { + "items": { + "$ref": "#/components/schemas/TimelineBranch" + }, + "title": "Branches", + "type": "array" + }, + "content": { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TimelineEvent" + }, + { + "$ref": "#/components/schemas/TimelineSpan" + } + ] + }, + "title": "Content", + "type": "array" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "id": { + "title": "Id", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "outline": { + "anyOf": [ + { + "$ref": "#/components/schemas/Outline" + }, + { + "type": "null" + } + ], + "default": null + }, + "span_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Span Type" + }, + "type": { + "const": "span", + "default": "span", + "title": "Type", + "type": "string" + }, + "utility": { + "default": false, + "title": "Utility", + "type": "boolean" + } + }, + "required": [ + "type", + "id", + "name", + "content", + "branches", + "utility" + ], + "title": "TimelineSpan", + "type": "object" + }, "ToolCall": { "properties": { "arguments": { @@ -9001,6 +9723,18 @@ "default": null, "title": "Agent" }, + "agent_span_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Agent Span Id" + }, "arguments": { "additionalProperties": { "$ref": "#/components/schemas/JsonValue" @@ -9126,6 +9860,9 @@ { "$ref": "#/components/schemas/ContentVideo" }, + { + "$ref": "#/components/schemas/ContentDocument" + }, { "items": { "anyOf": [ @@ -9140,6 +9877,9 @@ }, { "$ref": "#/components/schemas/ContentVideo" + }, + { + "$ref": "#/components/schemas/ContentDocument" } ] }, @@ -9647,6 +10387,13 @@ ], "title": "Task Set" }, + "timelines": { + "items": { + "$ref": "#/components/schemas/Timeline" + }, + "title": "Timelines", + "type": "array" + }, "total_time": { "anyOf": [ { @@ -9678,7 +10425,8 @@ "transcript_id", "metadata", "messages", - "events" + "events", + "timelines" ], "title": "Transcript", "type": "object" @@ -10948,7 +11696,7 @@ ] }, "get": { - "description": "Returns detailed status and metadata for a single scan.", + "description": "Returns detailed status and metadata for a single scan. Send Accept: application/zip to download the scan directory as a zip archive.", "operationId": "scan_scans__dir___scan__get", "parameters": [ { @@ -10981,6 +11729,9 @@ "schema": { "$ref": "#/components/schemas/Status" } + }, + "application/zip": { + "description": "Zip archive of the scan directory when Accept: application/zip is sent." } }, "description": "Successful Response" @@ -11049,7 +11800,7 @@ }, "/scans/{dir}/{scan}/{scanner}/{uuid}/input": { "get": { - "description": "Returns the original input text for a specific scanner result. The input type is returned in the X-Input-Type response header.", + "description": "Returns a JSON envelope with input, input_type, and input_data (EventsData pools for condensed events, or null).", "operationId": "scanner_input_scans__dir___scan___scanner___uuid__input_get", "parameters": [ { @@ -11101,13 +11852,15 @@ "200": { "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/ScannerInputResponse" + } } }, "description": "Successful Response" } }, - "summary": "Get scanner input for a specific transcript", + "summary": "Get scanner input for a specific result", "tags": [ "scans" ] @@ -11160,7 +11913,8 @@ "propertyNames": { "enum": [ "project-config", - "scans" + "scans", + "transcripts" ] }, "title": "Response Get Topics Topics Get", diff --git a/src/inspect_scout/_view/server.py b/src/inspect_scout/_view/server.py index 0007fa5d7..66e0ffff1 100644 --- a/src/inspect_scout/_view/server.py +++ b/src/inspect_scout/_view/server.py @@ -1,28 +1,62 @@ +import logging import os from pathlib import Path from typing import Any, Literal +from urllib.parse import urlparse import anyio import uvicorn from fastapi import FastAPI, Request, Response from fastapi.staticfiles import StaticFiles +from inspect_ai._lfs import LFSError, resolve_lfs_directory from inspect_ai._util.file import filesystem from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.responses import HTMLResponse from starlette.types import Scope +from inspect_scout._util.appdirs import scout_cache_dir from inspect_scout._util.constants import ( DEFAULT_SCANS_DIR, DEFAULT_SERVER_HOST, DEFAULT_VIEW_PORT, ) +from inspect_scout._view.notify import notify_lifespan from inspect_scout._view.types import ViewConfig from .._display._display import display from ._api_v2 import v2_api_app +_IMMUTABLE_CACHE = "public, max-age=31536000, immutable" +_DIST_DIR = Path(__file__).parent / "dist" +_REPO_URL = "https://github.com/meridianlabs-ai/inspect_scout.git" -class NoCacheStaticFiles(StaticFiles): - """StaticFiles that prevents caching of JS files during development.""" + +class _ScoutStaticFiles(StaticFiles): + """StaticFiles with runtime config injection and cache headers. + + Hashed assets under ``assets/`` are served with immutable cache headers. + When root_path is set, injects a script tag into index.html so the + frontend knows the base path for API requests behind a reverse proxy. + """ + + def __init__( + self, + *args: Any, + root_path: str = "", + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self._index_html_override: str | None = None + if root_path: + index_path = Path(str(self.directory)) / "index.html" + html = index_path.read_text() + script = ( + "" + ) + self._index_html_override = html.replace("", f"{script}", 1) def file_response( self, @@ -31,17 +65,36 @@ def file_response( scope: Scope, status_code: int = 200, ) -> Response: + if ( + self._index_html_override is not None + and Path(str(full_path)).name == "index.html" + ): + return HTMLResponse( + content=self._index_html_override, + status_code=status_code, + ) + response = super().file_response(full_path, stat_result, scope, status_code) + if "/assets/" in str(full_path): + response.headers["cache-control"] = _IMMUTABLE_CACHE + else: + response.headers["cache-control"] = "no-cache" + return response - # We have seen sporadic caching of the core JS file in safari though I - # wasn't able to consistently reproduce it. To be safe, disable caching - # for all JS files for the time being - if str(full_path).endswith(".js"): - response.headers["cache-control"] = "no-cache, no-store, must-revalidate" - response.headers["pragma"] = "no-cache" - response.headers["expires"] = "0" - return response +def _normalize_root_path(root_path: str) -> str: + """Extract path from a full URL and strip trailing slashes. + + If UVICORN_ROOT_PATH is set to a full URL like + ``https://host/s/session/p/proxy/``, normalize ASGI root_path to + just the path portion (``/s/session/p/proxy``). + """ + if not root_path: + return "" + parsed = urlparse(root_path) + if parsed.scheme: + root_path = parsed.path + return root_path.rstrip("/") def view_server( @@ -51,7 +104,10 @@ def view_server( mode: Literal["default", "scans"] = "default", authorization: str | None = None, fs_options: dict[str, Any] | None = None, + root_path: str = "", ) -> None: + root_path = _normalize_root_path(root_path) + # get filesystem and resolve scan_dir to full path config = config or ViewConfig() scans = config.scans_cli or config.project.scans or DEFAULT_SCANS_DIR @@ -65,19 +121,26 @@ def view_server( if authorization: v2_api.add_middleware(AuthorizationMiddleware, authorization=authorization) - app = FastAPI() + app = FastAPI(lifespan=notify_lifespan) app.mount("/api/v2", v2_api) - dist = Path(__file__).parent / "www" / "dist" app.mount( - "/", NoCacheStaticFiles(directory=dist.as_posix(), html=True), name="static" + "/", + _ScoutStaticFiles( + directory=_resolve_dist_directory().as_posix(), + html=True, + root_path=root_path, + ), + name="static", ) # run app display().print("Scout View") async def run_server() -> None: - config = uvicorn.Config(app, host=host, port=port, log_config=None) + config = uvicorn.Config( + app, host=host, port=port, root_path=root_path, log_config=None + ) server = uvicorn.Server(config) async def announce_when_ready() -> None: @@ -114,3 +177,54 @@ async def dispatch( if auth_header != self.authorization: return Response("Unauthorized", status_code=401) return await call_next(request) + + +def _resolve_dist_directory() -> Path: + """Resolve the frontend dist directory, downloading LFS objects if needed. + + To test without Git LFS (simulates a user without LFS installed):: + + # Enter test state — discard LFS content, leave pointer stubs + git lfs uninstall + GIT_LFS_SKIP_SMUDGE=1 git rm --cached -r src/inspect_scout/_view/dist + GIT_LFS_SKIP_SMUDGE=1 git reset --hard + # WARNING: git reset --hard discards uncommitted changes + + # Verify — LFS files should be ~130-byte pointer stubs + head -1 src/inspect_scout/_view/dist/index.html + # Expected: "version https://git-lfs.github.com/spec/v1" + + # Restore + git lfs install + git lfs pull + """ + # The LFS module logs download progress at INFO, but scout's default log + # display level is WARNING. Add a temporary handler so users see download + # activity during startup. + lfs_logger = logging.getLogger("inspect_ai._lfs") + prev_level = lfs_logger.level + lfs_logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + handler.setLevel(logging.INFO) + handler.setFormatter(logging.Formatter("%(message)s")) + lfs_logger.addHandler(handler) + try: + result = resolve_lfs_directory( + _DIST_DIR, + cache_dir=scout_cache_dir("dist"), + repo_url=_REPO_URL, + ) + if result != _DIST_DIR: + display().print(f"Serving static data from {result}") + + return result + except LFSError as e: + raise RuntimeError( + f"{e}\n" + "To fix this, either:\n" + " 1. Install Git LFS: brew install git-lfs && git lfs install && git lfs pull\n" + " 2. Build locally: cd src/inspect_scout/_view/ts-mono && pnpm build" + ) from e + finally: + lfs_logger.removeHandler(handler) + lfs_logger.setLevel(prev_level) diff --git a/src/inspect_scout/_view/ts-mono b/src/inspect_scout/_view/ts-mono new file mode 160000 index 000000000..22c65f1a7 --- /dev/null +++ b/src/inspect_scout/_view/ts-mono @@ -0,0 +1 @@ +Subproject commit 22c65f1a73619bdb99fda156670e426988e47e5b diff --git a/src/inspect_scout/_view/view.py b/src/inspect_scout/_view/view.py index c29d4d5bc..4f9893e4e 100644 --- a/src/inspect_scout/_view/view.py +++ b/src/inspect_scout/_view/view.py @@ -26,6 +26,7 @@ def view( authorization: str | None = None, log_level: str | None = None, fs_options: dict[str, Any] | None = None, + root_path: str = "", ) -> None: with chdir(project_dir or "."): # top level init @@ -51,4 +52,5 @@ def view( mode=mode, authorization=authorization, fs_options=fs_options, + root_path=root_path, ) diff --git a/src/inspect_scout/_view/www/.gitignore b/src/inspect_scout/_view/www/.gitignore deleted file mode 100644 index 9ae0b008a..000000000 --- a/src/inspect_scout/_view/www/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist/assets/*.js.map -test-results/ -playwright-report/ diff --git a/src/inspect_scout/_view/www/.nvmrc b/src/inspect_scout/_view/www/.nvmrc deleted file mode 100644 index 5767036af..000000000 --- a/src/inspect_scout/_view/www/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22.21.1 diff --git a/src/inspect_scout/_view/www/.prettierignore b/src/inspect_scout/_view/www/.prettierignore deleted file mode 100644 index 925f2ebf4..000000000 --- a/src/inspect_scout/_view/www/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -dist -node_modules -build -coverage -*.min.js -src/types/generated.ts diff --git a/src/inspect_scout/_view/www/.prettierrc b/src/inspect_scout/_view/www/.prettierrc deleted file mode 100644 index c4be887be..000000000 --- a/src/inspect_scout/_view/www/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "trailingComma": "es5" -} \ No newline at end of file diff --git a/src/inspect_scout/_view/www/README.md b/src/inspect_scout/_view/www/README.md deleted file mode 100644 index d82babf94..000000000 --- a/src/inspect_scout/_view/www/README.md +++ /dev/null @@ -1,142 +0,0 @@ -# Inspect Scout Viewer - -A React-based web viewer for Inspect AI evaluation logs. - -## Prerequisites - -This project uses [pnpm](https://pnpm.io/) as its package manager, managed through [corepack](https://nodejs.org/api/corepack.html). - -### Setup - -**Enable corepack** (required once): -```bash -corepack enable -``` - -That's it! Corepack is built into Node.js 16.9+ and will automatically install the correct pnpm version (specified in `package.json`) when you run pnpm commands. - -**Alternative:** If you prefer to install pnpm manually, see the [official pnpm installation guide](https://pnpm.io/installation). - -### Install Dependencies - -```bash -pnpm install -``` - -## Development - -Start the development server: - -```bash -pnpm dev -``` - -Build for production: - -```bash -pnpm build -``` - -Watch mode for development: - -```bash -pnpm watch -``` - -Preview production build: - -```bash -pnpm preview -``` - -## Code Quality - -Run linting: - -```bash -pnpm lint -``` - -Auto-fix linting issues: - -```bash -pnpm lint:fix -``` - -Format code: - -```bash -pnpm format -``` - -Check formatting: - -```bash -pnpm format:check -``` - -Type check: - -```bash -pnpm typecheck -``` - -Run all checks (lint, format, typecheck): - -```bash -pnpm check -``` - -## TypeScript Types from OpenAPI - -Types are auto-generated from the FastAPI OpenAPI spec to keep client/server in sync. - -### How It Works - -The type generation pipeline: -``` -Python Pydantic models → openapi.json → generated.ts → built app -``` - -1. **Export schema**: Python script exports OpenAPI spec from FastAPI to `openapi.json` -2. **Generate types**: `openapi-typescript` generates TypeScript types from schema -3. **Type adapter**: `src/types/api-types.ts` re-exports types with clean names -4. **Usage**: Import types from `src/types/index.ts` in your code - -### Updating Types After API Changes - -When Python Pydantic models change: - -```bash -# 1. Export updated OpenAPI schema -.venv/bin/python scripts/export_openapi_schema.py - -# 2. Types regenerate automatically on next build -pnpm build -``` - -Commit both `openapi.json` and `src/types/generated.ts`. - -### Manual Type Generation - -```bash -pnpm types:generate -``` - -### CI Validation - -CI automatically validates the type generation pipeline: - -- **`openapi-schema` job**: Regenerates `openapi.json` and fails if it differs from committed version -- **`js-build` job**: Regenerates `generated.ts` and fails if it differs from committed version - -If CI fails, run the commands shown in the error message and commit the updated files. - -## Tech Stack - -- React 19 -- TypeScript -- Vite -- Bootstrap 5 -- AG Grid -- React Router diff --git a/src/inspect_scout/_view/www/design/front-end-testing.md b/src/inspect_scout/_view/www/design/front-end-testing.md deleted file mode 100644 index a0d0b5996..000000000 --- a/src/inspect_scout/_view/www/design/front-end-testing.md +++ /dev/null @@ -1,68 +0,0 @@ -# Frontend Testing - -## Prefer Integration Tests - - -["Write tests, not too many, mostly integration."](https://kentcdodds.com/blog/write-tests) — Kent C. Dodds - -Integration tests exercise multiple units together and minimize mocking. This gives higher confidence per test — a single test that runs through the hook, React Query, and the API layer catches wiring bugs that isolated unit tests miss. Mocking internal modules (e.g., `vi.mock()`) couples tests to implementation details. - -## MSW (Mock Service Worker) - -Network-level mocking for API hook tests ([Stop mocking fetch](https://kentcdodds.com/blog/stop-mocking-fetch)). MSW intercepts `fetch` so the real API layer runs end-to-end. Request/response assertions verify what actually goes over the wire, and handlers are portable across test frameworks. - -For example, the `PUT` to `/project/config` requires an `If-Match` header for optimistic concurrency. A `vi.mock()` test that stubs `api.updateProjectConfig()` would pass even if the API layer forgot the header — the real `fetch` never fires. An MSW test catches this because it inspects the actual request: - -```typescript -http.put("/api/v2/project/config", async ({ request }) => { - capturedHeaders = new Headers(request.headers); - return HttpResponse.json(updatedConfig); -}); - -// Fails if the API layer drops the header -expect(capturedHeaders?.get("If-Match")).toBe('"v1"'); -``` - -| File | Purpose | -|------|---------| -| `src/test/setup-msw.ts` | Server lifecycle — `beforeAll`/`afterEach`/`afterAll` | -| `src/test/test-utils.tsx` | `createTestWrapper()` — provides QueryClient + ApiProvider | - -**Setup**: MSW runs automatically via `vitest.config.ts` `setupFiles`. No per-test setup needed. - -**Config**: `onUnhandledRequest: "error"` — any fetch not handled by a test handler fails the test. QueryClient uses `retry: false`, `gcTime: 0` for deterministic behavior. - -**Collapsed group suppression**: Node has no concept of `console.groupCollapsed` — it prints everything. `setup-msw.ts` replicates browser behavior by suppressing `console.log` calls inside collapsed groups. This quiets MSW's SSE handler (which wraps all logging in `groupCollapsed`), but applies to any code using collapsed groups. Top-level `console.log` is unaffected. - - -### Type-check mock response data - -Always pass the response type to `HttpResponse.json()`. Without it, mock data silently drifts from the real API schema — missing fields, wrong types, stale shapes after a backend change — and tests pass against payloads the server would never return. - -```typescript -http.get("/api/v2/scans/active", () => - HttpResponse.json({ - items: { "scan-123": scanInfo }, - }), -); -``` - -## Patterns - -### Hooks - -**Shared QueryClient across hooks**: Pass the same `wrapper` to multiple `renderHook` calls. -```typescript -const wrapper = createTestWrapper(); -const { result: queryResult } = renderHook(() => useQuery(...), { wrapper }); -const { result: mutationResult } = renderHook(() => useMutation(...), { wrapper }); -``` - -**skipToken**: Verify the query stays in loading state and no request is made. -```typescript -server.use(http.get("/api/v2/endpoint", () => { requestMade = true; ... })); -renderHook(() => useHook(skipToken), { wrapper: createTestWrapper() }); -expect(result.current.loading).toBe(true); -await new Promise((r) => setTimeout(r, 50)); -expect(requestMade).toBe(false); -``` \ No newline at end of file diff --git a/src/inspect_scout/_view/www/design/react-query.md b/src/inspect_scout/_view/www/design/react-query.md deleted file mode 100644 index f99dc9b5a..000000000 --- a/src/inspect_scout/_view/www/design/react-query.md +++ /dev/null @@ -1,117 +0,0 @@ -# React Query Patterns - -This document covers type-safe patterns for using TanStack Query (React Query) v5 in the frontend. - -## Type-Safe Query Keys with `queryOptions` - -### The Problem - -React Query's `queryClient.setQueryData()` and `getQueryData()` don't inherently enforce type consistency. You can write data of one type and read it expecting another: - -```typescript -// No compile-time error, but runtime disaster -queryClient.setQueryData(["scan", dir, path], scanRowData); // ScanRow -const data = queryClient.getQueryData(["scan", dir, path]); // Expects Status -``` - -### The Solution: DataTag - -React Query v5 introduced `queryOptions()` which brands the query key with a `DataTag` containing type information: - -```typescript -import { queryOptions } from "@tanstack/react-query"; - -export const scanDetailOptions = (api: ScanApi, scansDir: string, scanPath: string) => - queryOptions({ - queryKey: ["scan", scansDir, scanPath] as const, - queryFn: (): Promise => api.getScan(scansDir, scanPath), - staleTime: 10000, - }); -``` - -The returned `queryKey` is typed as `DataTag<["scan", string, string], Status, Error>`. - -### Type-Safe Cache Operations - -When you use the tagged key, TypeScript enforces type consistency: - -```typescript -const options = scanDetailOptions(api, scansDir, scanPath); - -// TypeScript infers the correct type -const data = queryClient.getQueryData(options.queryKey); -// ^? Status | undefined - -// Compile error: ScanRow is not assignable to Status -queryClient.setQueryData(options.queryKey, scanRowData); -``` - -## Query Options Factory Pattern - -Define query options in a centralized file for reuse: - -```typescript -// src/app/server/scanQueries.ts -import { queryOptions } from "@tanstack/react-query"; - -export const scansListOptions = (api: ScanApi, scansDir: string) => - queryOptions({ - queryKey: ["scans", scansDir] as const, - queryFn: async (): Promise => { - const response = await api.getScans(scansDir); - return response.items; - }, - staleTime: 5000, - refetchInterval: 5000, - }); -``` - -Then use in hooks: - -```typescript -export const useScans = (scansDir: string): AsyncData => { - const api = useApi(); - return useAsyncDataFromQuery(scansListOptions(api, scansDir)); -}; -``` - -## Conditional Queries with `skipToken` - -`skipToken` is used to disable a query until required parameters are available. This is common for dependent queries where you need to wait for user selection or parent data before fetching. - -### Example: Fetching a Selected Scan - -```typescript -import { skipToken } from "@tanstack/react-query"; - -type ScanParams = { scansDir: string; scanPath: string }; - -// The hook accepts either valid params or skipToken -export const useScan = ( - params: ScanParams | typeof skipToken -): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: - params === skipToken - ? [skipToken] // All disabled queries share this key - : ["scan", params.scansDir, params.scanPath], - queryFn: - params === skipToken - ? skipToken // Query won't execute - : () => api.getScan(params.scansDir, params.scanPath), - staleTime: 10000, - }); -}; - -// Usage: query is disabled until scanPath is available -const { data } = useScan( - scanPath ? { scansDir, scanPath } : skipToken -); -``` - -## References - -- [The Query Options API - TkDodo](https://tkdodo.eu/blog/the-query-options-api) -- [Type-safe React Query - TkDodo](https://tkdodo.eu/blog/type-safe-react-query) diff --git a/src/inspect_scout/_view/www/dist/assets/_commonjsHelpers.js b/src/inspect_scout/_view/www/dist/assets/_commonjsHelpers.js deleted file mode 100644 index 7cea3d116..000000000 --- a/src/inspect_scout/_view/www/dist/assets/_commonjsHelpers.js +++ /dev/null @@ -1,9 +0,0 @@ -var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {}; -function getDefaultExportFromCjs(x) { - return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x; -} -export { - commonjsGlobal as c, - getDefaultExportFromCjs as g -}; -//# sourceMappingURL=_commonjsHelpers.js.map diff --git a/src/inspect_scout/_view/www/dist/assets/chunk-DfAF0w94.js b/src/inspect_scout/_view/www/dist/assets/chunk-DfAF0w94.js deleted file mode 100644 index 911830fb4..000000000 --- a/src/inspect_scout/_view/www/dist/assets/chunk-DfAF0w94.js +++ /dev/null @@ -1,9 +0,0 @@ -var e = Object.create, t = Object.defineProperty, n = Object.getOwnPropertyDescriptor, r = Object.getOwnPropertyNames, i = Object.getPrototypeOf, a = Object.prototype.hasOwnProperty, o = (e2, t2) => () => (t2 || e2((t2 = { exports: {} }).exports, t2), t2.exports), s = (e2, i2, o2, s2) => { - if (i2 && typeof i2 == `object` || typeof i2 == `function`) for (var c2 = r(i2), l2 = 0, u = c2.length, d; l2 < u; l2++) d = c2[l2], !a.call(e2, d) && d !== o2 && t(e2, d, { get: ((e3) => i2[e3]).bind(null, d), enumerable: !(s2 = n(i2, d)) || s2.enumerable }); - return e2; -}, c = (n2, r2, a2) => (a2 = n2 == null ? {} : e(i(n2)), s(t(a2, `default`, { value: n2, enumerable: true }), n2)), l = (e2) => (t2) => c(t2.default); -export { - l, - o -}; -//# sourceMappingURL=chunk-DfAF0w94.js.map diff --git a/src/inspect_scout/_view/www/dist/assets/favicon.svg b/src/inspect_scout/_view/www/dist/assets/favicon.svg deleted file mode 100644 index 95ca1a99b..000000000 --- a/src/inspect_scout/_view/www/dist/assets/favicon.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/inspect_scout/_view/www/dist/assets/index.css b/src/inspect_scout/_view/www/dist/assets/index.css deleted file mode 100644 index c70a44c64..000000000 --- a/src/inspect_scout/_view/www/dist/assets/index.css +++ /dev/null @@ -1,10126 +0,0 @@ -@charset "UTF-8";/*! - * Bootstrap Icons v1.13.1 (https://icons.getbootstrap.com/) - * Copyright 2019-2024 The Bootstrap Authors - * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) - */ - -@font-face { - font-display: block; - font-family: "bootstrap-icons"; - src: url("data:font/woff2;base64,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") format("woff2"), -url("data:font/woff;base64,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") format("woff"); -} - -.bi::before, -[class^="bi-"]::before, -[class*=" bi-"]::before { - display: inline-block; - font-family: bootstrap-icons !important; - font-style: normal; - font-weight: normal !important; - font-variant: normal; - text-transform: none; - line-height: 1; - vertical-align: -.125em; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.bi-123::before { content: "\f67f"; } -.bi-alarm-fill::before { content: "\f101"; } -.bi-alarm::before { content: "\f102"; } -.bi-align-bottom::before { content: "\f103"; } -.bi-align-center::before { content: "\f104"; } -.bi-align-end::before { content: "\f105"; } -.bi-align-middle::before { content: "\f106"; } -.bi-align-start::before { content: "\f107"; } -.bi-align-top::before { content: "\f108"; } -.bi-alt::before { content: "\f109"; } -.bi-app-indicator::before { content: "\f10a"; } -.bi-app::before { content: "\f10b"; } -.bi-archive-fill::before { content: "\f10c"; } -.bi-archive::before { content: "\f10d"; } -.bi-arrow-90deg-down::before { content: "\f10e"; } -.bi-arrow-90deg-left::before { content: "\f10f"; } -.bi-arrow-90deg-right::before { content: "\f110"; } -.bi-arrow-90deg-up::before { content: "\f111"; } -.bi-arrow-bar-down::before { content: "\f112"; } -.bi-arrow-bar-left::before { content: "\f113"; } -.bi-arrow-bar-right::before { content: "\f114"; } -.bi-arrow-bar-up::before { content: "\f115"; } -.bi-arrow-clockwise::before { content: "\f116"; } -.bi-arrow-counterclockwise::before { content: "\f117"; } -.bi-arrow-down-circle-fill::before { content: "\f118"; } -.bi-arrow-down-circle::before { content: "\f119"; } -.bi-arrow-down-left-circle-fill::before { content: "\f11a"; } -.bi-arrow-down-left-circle::before { content: "\f11b"; } -.bi-arrow-down-left-square-fill::before { content: "\f11c"; } -.bi-arrow-down-left-square::before { content: "\f11d"; } -.bi-arrow-down-left::before { content: "\f11e"; } -.bi-arrow-down-right-circle-fill::before { content: "\f11f"; } -.bi-arrow-down-right-circle::before { content: "\f120"; } -.bi-arrow-down-right-square-fill::before { content: "\f121"; } -.bi-arrow-down-right-square::before { content: "\f122"; } -.bi-arrow-down-right::before { content: "\f123"; } -.bi-arrow-down-short::before { content: "\f124"; } -.bi-arrow-down-square-fill::before { content: "\f125"; } -.bi-arrow-down-square::before { content: "\f126"; } -.bi-arrow-down-up::before { content: "\f127"; } -.bi-arrow-down::before { content: "\f128"; } -.bi-arrow-left-circle-fill::before { content: "\f129"; } -.bi-arrow-left-circle::before { content: "\f12a"; } -.bi-arrow-left-right::before { content: "\f12b"; } -.bi-arrow-left-short::before { content: "\f12c"; } -.bi-arrow-left-square-fill::before { content: "\f12d"; } -.bi-arrow-left-square::before { content: "\f12e"; } -.bi-arrow-left::before { content: "\f12f"; } -.bi-arrow-repeat::before { content: "\f130"; } -.bi-arrow-return-left::before { content: "\f131"; } -.bi-arrow-return-right::before { content: "\f132"; } -.bi-arrow-right-circle-fill::before { content: "\f133"; } -.bi-arrow-right-circle::before { content: "\f134"; } -.bi-arrow-right-short::before { content: "\f135"; } -.bi-arrow-right-square-fill::before { content: "\f136"; } -.bi-arrow-right-square::before { content: "\f137"; } -.bi-arrow-right::before { content: "\f138"; } -.bi-arrow-up-circle-fill::before { content: "\f139"; } -.bi-arrow-up-circle::before { content: "\f13a"; } -.bi-arrow-up-left-circle-fill::before { content: "\f13b"; } -.bi-arrow-up-left-circle::before { content: "\f13c"; } -.bi-arrow-up-left-square-fill::before { content: "\f13d"; } -.bi-arrow-up-left-square::before { content: "\f13e"; } -.bi-arrow-up-left::before { content: "\f13f"; } -.bi-arrow-up-right-circle-fill::before { content: "\f140"; } -.bi-arrow-up-right-circle::before { content: "\f141"; } -.bi-arrow-up-right-square-fill::before { content: "\f142"; } -.bi-arrow-up-right-square::before { content: "\f143"; } -.bi-arrow-up-right::before { content: "\f144"; } -.bi-arrow-up-short::before { content: "\f145"; } -.bi-arrow-up-square-fill::before { content: "\f146"; } -.bi-arrow-up-square::before { content: "\f147"; } -.bi-arrow-up::before { content: "\f148"; } -.bi-arrows-angle-contract::before { content: "\f149"; } -.bi-arrows-angle-expand::before { content: "\f14a"; } -.bi-arrows-collapse::before { content: "\f14b"; } -.bi-arrows-expand::before { content: "\f14c"; } -.bi-arrows-fullscreen::before { content: "\f14d"; } -.bi-arrows-move::before { content: "\f14e"; } -.bi-aspect-ratio-fill::before { content: "\f14f"; } -.bi-aspect-ratio::before { content: "\f150"; } -.bi-asterisk::before { content: "\f151"; } -.bi-at::before { content: "\f152"; } -.bi-award-fill::before { content: "\f153"; } -.bi-award::before { content: "\f154"; } -.bi-back::before { content: "\f155"; } -.bi-backspace-fill::before { content: "\f156"; } -.bi-backspace-reverse-fill::before { content: "\f157"; } -.bi-backspace-reverse::before { content: "\f158"; } -.bi-backspace::before { content: "\f159"; } -.bi-badge-3d-fill::before { content: "\f15a"; } -.bi-badge-3d::before { content: "\f15b"; } -.bi-badge-4k-fill::before { content: "\f15c"; } -.bi-badge-4k::before { content: "\f15d"; } -.bi-badge-8k-fill::before { content: "\f15e"; } -.bi-badge-8k::before { content: "\f15f"; } -.bi-badge-ad-fill::before { content: "\f160"; } -.bi-badge-ad::before { content: "\f161"; } -.bi-badge-ar-fill::before { content: "\f162"; } -.bi-badge-ar::before { content: "\f163"; } -.bi-badge-cc-fill::before { content: "\f164"; } -.bi-badge-cc::before { content: "\f165"; } -.bi-badge-hd-fill::before { content: "\f166"; } -.bi-badge-hd::before { content: "\f167"; } -.bi-badge-tm-fill::before { content: "\f168"; } -.bi-badge-tm::before { content: "\f169"; } -.bi-badge-vo-fill::before { content: "\f16a"; } -.bi-badge-vo::before { content: "\f16b"; } -.bi-badge-vr-fill::before { content: "\f16c"; } -.bi-badge-vr::before { content: "\f16d"; } -.bi-badge-wc-fill::before { content: "\f16e"; } -.bi-badge-wc::before { content: "\f16f"; } -.bi-bag-check-fill::before { content: "\f170"; } -.bi-bag-check::before { content: "\f171"; } -.bi-bag-dash-fill::before { content: "\f172"; } -.bi-bag-dash::before { content: "\f173"; } -.bi-bag-fill::before { content: "\f174"; } -.bi-bag-plus-fill::before { content: "\f175"; } -.bi-bag-plus::before { content: "\f176"; } -.bi-bag-x-fill::before { content: "\f177"; } -.bi-bag-x::before { content: "\f178"; } -.bi-bag::before { content: "\f179"; } -.bi-bar-chart-fill::before { content: "\f17a"; } -.bi-bar-chart-line-fill::before { content: "\f17b"; } -.bi-bar-chart-line::before { content: "\f17c"; } -.bi-bar-chart-steps::before { content: "\f17d"; } -.bi-bar-chart::before { content: "\f17e"; } -.bi-basket-fill::before { content: "\f17f"; } -.bi-basket::before { content: "\f180"; } -.bi-basket2-fill::before { content: "\f181"; } -.bi-basket2::before { content: "\f182"; } -.bi-basket3-fill::before { content: "\f183"; } -.bi-basket3::before { content: "\f184"; } -.bi-battery-charging::before { content: "\f185"; } -.bi-battery-full::before { content: "\f186"; } -.bi-battery-half::before { content: "\f187"; } -.bi-battery::before { content: "\f188"; } -.bi-bell-fill::before { content: "\f189"; } -.bi-bell::before { content: "\f18a"; } -.bi-bezier::before { content: "\f18b"; } -.bi-bezier2::before { content: "\f18c"; } -.bi-bicycle::before { content: "\f18d"; } -.bi-binoculars-fill::before { content: "\f18e"; } -.bi-binoculars::before { content: "\f18f"; } -.bi-blockquote-left::before { content: "\f190"; } -.bi-blockquote-right::before { content: "\f191"; } -.bi-book-fill::before { content: "\f192"; } -.bi-book-half::before { content: "\f193"; } -.bi-book::before { content: "\f194"; } -.bi-bookmark-check-fill::before { content: "\f195"; } -.bi-bookmark-check::before { content: "\f196"; } -.bi-bookmark-dash-fill::before { content: "\f197"; } -.bi-bookmark-dash::before { content: "\f198"; } -.bi-bookmark-fill::before { content: "\f199"; } -.bi-bookmark-heart-fill::before { content: "\f19a"; } -.bi-bookmark-heart::before { content: "\f19b"; } -.bi-bookmark-plus-fill::before { content: "\f19c"; } -.bi-bookmark-plus::before { content: "\f19d"; } -.bi-bookmark-star-fill::before { content: "\f19e"; } -.bi-bookmark-star::before { content: "\f19f"; } -.bi-bookmark-x-fill::before { content: "\f1a0"; } -.bi-bookmark-x::before { content: "\f1a1"; } -.bi-bookmark::before { content: "\f1a2"; } -.bi-bookmarks-fill::before { content: "\f1a3"; } -.bi-bookmarks::before { content: "\f1a4"; } -.bi-bookshelf::before { content: "\f1a5"; } -.bi-bootstrap-fill::before { content: "\f1a6"; } -.bi-bootstrap-reboot::before { content: "\f1a7"; } -.bi-bootstrap::before { content: "\f1a8"; } -.bi-border-all::before { content: "\f1a9"; } -.bi-border-bottom::before { content: "\f1aa"; } -.bi-border-center::before { content: "\f1ab"; } -.bi-border-inner::before { content: "\f1ac"; } -.bi-border-left::before { content: "\f1ad"; } -.bi-border-middle::before { content: "\f1ae"; } -.bi-border-outer::before { content: "\f1af"; } -.bi-border-right::before { content: "\f1b0"; } -.bi-border-style::before { content: "\f1b1"; } -.bi-border-top::before { content: "\f1b2"; } -.bi-border-width::before { content: "\f1b3"; } -.bi-border::before { content: "\f1b4"; } -.bi-bounding-box-circles::before { content: "\f1b5"; } -.bi-bounding-box::before { content: "\f1b6"; } -.bi-box-arrow-down-left::before { content: "\f1b7"; } -.bi-box-arrow-down-right::before { content: "\f1b8"; } -.bi-box-arrow-down::before { content: "\f1b9"; } -.bi-box-arrow-in-down-left::before { content: "\f1ba"; } -.bi-box-arrow-in-down-right::before { content: "\f1bb"; } -.bi-box-arrow-in-down::before { content: "\f1bc"; } -.bi-box-arrow-in-left::before { content: "\f1bd"; } -.bi-box-arrow-in-right::before { content: "\f1be"; } -.bi-box-arrow-in-up-left::before { content: "\f1bf"; } -.bi-box-arrow-in-up-right::before { content: "\f1c0"; } -.bi-box-arrow-in-up::before { content: "\f1c1"; } -.bi-box-arrow-left::before { content: "\f1c2"; } -.bi-box-arrow-right::before { content: "\f1c3"; } -.bi-box-arrow-up-left::before { content: "\f1c4"; } -.bi-box-arrow-up-right::before { content: "\f1c5"; } -.bi-box-arrow-up::before { content: "\f1c6"; } -.bi-box-seam::before { content: "\f1c7"; } -.bi-box::before { content: "\f1c8"; } -.bi-braces::before { content: "\f1c9"; } -.bi-bricks::before { content: "\f1ca"; } -.bi-briefcase-fill::before { content: "\f1cb"; } -.bi-briefcase::before { content: "\f1cc"; } -.bi-brightness-alt-high-fill::before { content: "\f1cd"; } -.bi-brightness-alt-high::before { content: "\f1ce"; } -.bi-brightness-alt-low-fill::before { content: "\f1cf"; } -.bi-brightness-alt-low::before { content: "\f1d0"; } -.bi-brightness-high-fill::before { content: "\f1d1"; } -.bi-brightness-high::before { content: "\f1d2"; } -.bi-brightness-low-fill::before { content: "\f1d3"; } -.bi-brightness-low::before { content: "\f1d4"; } -.bi-broadcast-pin::before { content: "\f1d5"; } -.bi-broadcast::before { content: "\f1d6"; } -.bi-brush-fill::before { content: "\f1d7"; } -.bi-brush::before { content: "\f1d8"; } -.bi-bucket-fill::before { content: "\f1d9"; } -.bi-bucket::before { content: "\f1da"; } -.bi-bug-fill::before { content: "\f1db"; } -.bi-bug::before { content: "\f1dc"; } -.bi-building::before { content: "\f1dd"; } -.bi-bullseye::before { content: "\f1de"; } -.bi-calculator-fill::before { content: "\f1df"; } -.bi-calculator::before { content: "\f1e0"; } -.bi-calendar-check-fill::before { content: "\f1e1"; } -.bi-calendar-check::before { content: "\f1e2"; } -.bi-calendar-date-fill::before { content: "\f1e3"; } -.bi-calendar-date::before { content: "\f1e4"; } -.bi-calendar-day-fill::before { content: "\f1e5"; } -.bi-calendar-day::before { content: "\f1e6"; } -.bi-calendar-event-fill::before { content: "\f1e7"; } -.bi-calendar-event::before { content: "\f1e8"; } -.bi-calendar-fill::before { content: "\f1e9"; } -.bi-calendar-minus-fill::before { content: "\f1ea"; } -.bi-calendar-minus::before { content: "\f1eb"; } -.bi-calendar-month-fill::before { content: "\f1ec"; } -.bi-calendar-month::before { content: "\f1ed"; } -.bi-calendar-plus-fill::before { content: "\f1ee"; } -.bi-calendar-plus::before { content: "\f1ef"; } -.bi-calendar-range-fill::before { content: "\f1f0"; } -.bi-calendar-range::before { content: "\f1f1"; } -.bi-calendar-week-fill::before { content: "\f1f2"; } -.bi-calendar-week::before { content: "\f1f3"; } -.bi-calendar-x-fill::before { content: "\f1f4"; } -.bi-calendar-x::before { content: "\f1f5"; } -.bi-calendar::before { content: "\f1f6"; } -.bi-calendar2-check-fill::before { content: "\f1f7"; } -.bi-calendar2-check::before { content: "\f1f8"; } -.bi-calendar2-date-fill::before { content: "\f1f9"; } -.bi-calendar2-date::before { content: "\f1fa"; } -.bi-calendar2-day-fill::before { content: "\f1fb"; } -.bi-calendar2-day::before { content: "\f1fc"; } -.bi-calendar2-event-fill::before { content: "\f1fd"; } -.bi-calendar2-event::before { content: "\f1fe"; } -.bi-calendar2-fill::before { content: "\f1ff"; } -.bi-calendar2-minus-fill::before { content: "\f200"; } -.bi-calendar2-minus::before { content: "\f201"; } -.bi-calendar2-month-fill::before { content: "\f202"; } -.bi-calendar2-month::before { content: "\f203"; } -.bi-calendar2-plus-fill::before { content: "\f204"; } -.bi-calendar2-plus::before { content: "\f205"; } -.bi-calendar2-range-fill::before { content: "\f206"; } -.bi-calendar2-range::before { content: "\f207"; } -.bi-calendar2-week-fill::before { content: "\f208"; } -.bi-calendar2-week::before { content: "\f209"; } -.bi-calendar2-x-fill::before { content: "\f20a"; } -.bi-calendar2-x::before { content: "\f20b"; } -.bi-calendar2::before { content: "\f20c"; } -.bi-calendar3-event-fill::before { content: "\f20d"; } -.bi-calendar3-event::before { content: "\f20e"; } -.bi-calendar3-fill::before { content: "\f20f"; } -.bi-calendar3-range-fill::before { content: "\f210"; } -.bi-calendar3-range::before { content: "\f211"; } -.bi-calendar3-week-fill::before { content: "\f212"; } -.bi-calendar3-week::before { content: "\f213"; } -.bi-calendar3::before { content: "\f214"; } -.bi-calendar4-event::before { content: "\f215"; } -.bi-calendar4-range::before { content: "\f216"; } -.bi-calendar4-week::before { content: "\f217"; } -.bi-calendar4::before { content: "\f218"; } -.bi-camera-fill::before { content: "\f219"; } -.bi-camera-reels-fill::before { content: "\f21a"; } -.bi-camera-reels::before { content: "\f21b"; } -.bi-camera-video-fill::before { content: "\f21c"; } -.bi-camera-video-off-fill::before { content: "\f21d"; } -.bi-camera-video-off::before { content: "\f21e"; } -.bi-camera-video::before { content: "\f21f"; } -.bi-camera::before { content: "\f220"; } -.bi-camera2::before { content: "\f221"; } -.bi-capslock-fill::before { content: "\f222"; } -.bi-capslock::before { content: "\f223"; } -.bi-card-checklist::before { content: "\f224"; } -.bi-card-heading::before { content: "\f225"; } -.bi-card-image::before { content: "\f226"; } -.bi-card-list::before { content: "\f227"; } -.bi-card-text::before { content: "\f228"; } -.bi-caret-down-fill::before { content: "\f229"; } -.bi-caret-down-square-fill::before { content: "\f22a"; } -.bi-caret-down-square::before { content: "\f22b"; } -.bi-caret-down::before { content: "\f22c"; } -.bi-caret-left-fill::before { content: "\f22d"; } -.bi-caret-left-square-fill::before { content: "\f22e"; } -.bi-caret-left-square::before { content: "\f22f"; } -.bi-caret-left::before { content: "\f230"; } -.bi-caret-right-fill::before { content: "\f231"; } -.bi-caret-right-square-fill::before { content: "\f232"; } -.bi-caret-right-square::before { content: "\f233"; } -.bi-caret-right::before { content: "\f234"; } -.bi-caret-up-fill::before { content: "\f235"; } -.bi-caret-up-square-fill::before { content: "\f236"; } -.bi-caret-up-square::before { content: "\f237"; } -.bi-caret-up::before { content: "\f238"; } -.bi-cart-check-fill::before { content: "\f239"; } -.bi-cart-check::before { content: "\f23a"; } -.bi-cart-dash-fill::before { content: "\f23b"; } -.bi-cart-dash::before { content: "\f23c"; } -.bi-cart-fill::before { content: "\f23d"; } -.bi-cart-plus-fill::before { content: "\f23e"; } -.bi-cart-plus::before { content: "\f23f"; } -.bi-cart-x-fill::before { content: "\f240"; } -.bi-cart-x::before { content: "\f241"; } -.bi-cart::before { content: "\f242"; } -.bi-cart2::before { content: "\f243"; } -.bi-cart3::before { content: "\f244"; } -.bi-cart4::before { content: "\f245"; } -.bi-cash-stack::before { content: "\f246"; } -.bi-cash::before { content: "\f247"; } -.bi-cast::before { content: "\f248"; } -.bi-chat-dots-fill::before { content: "\f249"; } -.bi-chat-dots::before { content: "\f24a"; } -.bi-chat-fill::before { content: "\f24b"; } -.bi-chat-left-dots-fill::before { content: "\f24c"; } -.bi-chat-left-dots::before { content: "\f24d"; } -.bi-chat-left-fill::before { content: "\f24e"; } -.bi-chat-left-quote-fill::before { content: "\f24f"; } -.bi-chat-left-quote::before { content: "\f250"; } -.bi-chat-left-text-fill::before { content: "\f251"; } -.bi-chat-left-text::before { content: "\f252"; } -.bi-chat-left::before { content: "\f253"; } -.bi-chat-quote-fill::before { content: "\f254"; } -.bi-chat-quote::before { content: "\f255"; } -.bi-chat-right-dots-fill::before { content: "\f256"; } -.bi-chat-right-dots::before { content: "\f257"; } -.bi-chat-right-fill::before { content: "\f258"; } -.bi-chat-right-quote-fill::before { content: "\f259"; } -.bi-chat-right-quote::before { content: "\f25a"; } -.bi-chat-right-text-fill::before { content: "\f25b"; } -.bi-chat-right-text::before { content: "\f25c"; } -.bi-chat-right::before { content: "\f25d"; } -.bi-chat-square-dots-fill::before { content: "\f25e"; } -.bi-chat-square-dots::before { content: "\f25f"; } -.bi-chat-square-fill::before { content: "\f260"; } -.bi-chat-square-quote-fill::before { content: "\f261"; } -.bi-chat-square-quote::before { content: "\f262"; } -.bi-chat-square-text-fill::before { content: "\f263"; } -.bi-chat-square-text::before { content: "\f264"; } -.bi-chat-square::before { content: "\f265"; } -.bi-chat-text-fill::before { content: "\f266"; } -.bi-chat-text::before { content: "\f267"; } -.bi-chat::before { content: "\f268"; } -.bi-check-all::before { content: "\f269"; } -.bi-check-circle-fill::before { content: "\f26a"; } -.bi-check-circle::before { content: "\f26b"; } -.bi-check-square-fill::before { content: "\f26c"; } -.bi-check-square::before { content: "\f26d"; } -.bi-check::before { content: "\f26e"; } -.bi-check2-all::before { content: "\f26f"; } -.bi-check2-circle::before { content: "\f270"; } -.bi-check2-square::before { content: "\f271"; } -.bi-check2::before { content: "\f272"; } -.bi-chevron-bar-contract::before { content: "\f273"; } -.bi-chevron-bar-down::before { content: "\f274"; } -.bi-chevron-bar-expand::before { content: "\f275"; } -.bi-chevron-bar-left::before { content: "\f276"; } -.bi-chevron-bar-right::before { content: "\f277"; } -.bi-chevron-bar-up::before { content: "\f278"; } -.bi-chevron-compact-down::before { content: "\f279"; } -.bi-chevron-compact-left::before { content: "\f27a"; } -.bi-chevron-compact-right::before { content: "\f27b"; } -.bi-chevron-compact-up::before { content: "\f27c"; } -.bi-chevron-contract::before { content: "\f27d"; } -.bi-chevron-double-down::before { content: "\f27e"; } -.bi-chevron-double-left::before { content: "\f27f"; } -.bi-chevron-double-right::before { content: "\f280"; } -.bi-chevron-double-up::before { content: "\f281"; } -.bi-chevron-down::before { content: "\f282"; } -.bi-chevron-expand::before { content: "\f283"; } -.bi-chevron-left::before { content: "\f284"; } -.bi-chevron-right::before { content: "\f285"; } -.bi-chevron-up::before { content: "\f286"; } -.bi-circle-fill::before { content: "\f287"; } -.bi-circle-half::before { content: "\f288"; } -.bi-circle-square::before { content: "\f289"; } -.bi-circle::before { content: "\f28a"; } -.bi-clipboard-check::before { content: "\f28b"; } -.bi-clipboard-data::before { content: "\f28c"; } -.bi-clipboard-minus::before { content: "\f28d"; } -.bi-clipboard-plus::before { content: "\f28e"; } -.bi-clipboard-x::before { content: "\f28f"; } -.bi-clipboard::before { content: "\f290"; } -.bi-clock-fill::before { content: "\f291"; } -.bi-clock-history::before { content: "\f292"; } -.bi-clock::before { content: "\f293"; } -.bi-cloud-arrow-down-fill::before { content: "\f294"; } -.bi-cloud-arrow-down::before { content: "\f295"; } -.bi-cloud-arrow-up-fill::before { content: "\f296"; } -.bi-cloud-arrow-up::before { content: "\f297"; } -.bi-cloud-check-fill::before { content: "\f298"; } -.bi-cloud-check::before { content: "\f299"; } -.bi-cloud-download-fill::before { content: "\f29a"; } -.bi-cloud-download::before { content: "\f29b"; } -.bi-cloud-drizzle-fill::before { content: "\f29c"; } -.bi-cloud-drizzle::before { content: "\f29d"; } -.bi-cloud-fill::before { content: "\f29e"; } -.bi-cloud-fog-fill::before { content: "\f29f"; } -.bi-cloud-fog::before { content: "\f2a0"; } -.bi-cloud-fog2-fill::before { content: "\f2a1"; } -.bi-cloud-fog2::before { content: "\f2a2"; } -.bi-cloud-hail-fill::before { content: "\f2a3"; } -.bi-cloud-hail::before { content: "\f2a4"; } -.bi-cloud-haze-fill::before { content: "\f2a6"; } -.bi-cloud-haze::before { content: "\f2a7"; } -.bi-cloud-haze2-fill::before { content: "\f2a8"; } -.bi-cloud-lightning-fill::before { content: "\f2a9"; } -.bi-cloud-lightning-rain-fill::before { content: "\f2aa"; } -.bi-cloud-lightning-rain::before { content: "\f2ab"; } -.bi-cloud-lightning::before { content: "\f2ac"; } -.bi-cloud-minus-fill::before { content: "\f2ad"; } -.bi-cloud-minus::before { content: "\f2ae"; } -.bi-cloud-moon-fill::before { content: "\f2af"; } -.bi-cloud-moon::before { content: "\f2b0"; } -.bi-cloud-plus-fill::before { content: "\f2b1"; } -.bi-cloud-plus::before { content: "\f2b2"; } -.bi-cloud-rain-fill::before { content: "\f2b3"; } -.bi-cloud-rain-heavy-fill::before { content: "\f2b4"; } -.bi-cloud-rain-heavy::before { content: "\f2b5"; } -.bi-cloud-rain::before { content: "\f2b6"; } -.bi-cloud-slash-fill::before { content: "\f2b7"; } -.bi-cloud-slash::before { content: "\f2b8"; } -.bi-cloud-sleet-fill::before { content: "\f2b9"; } -.bi-cloud-sleet::before { content: "\f2ba"; } -.bi-cloud-snow-fill::before { content: "\f2bb"; } -.bi-cloud-snow::before { content: "\f2bc"; } -.bi-cloud-sun-fill::before { content: "\f2bd"; } -.bi-cloud-sun::before { content: "\f2be"; } -.bi-cloud-upload-fill::before { content: "\f2bf"; } -.bi-cloud-upload::before { content: "\f2c0"; } -.bi-cloud::before { content: "\f2c1"; } -.bi-clouds-fill::before { content: "\f2c2"; } -.bi-clouds::before { content: "\f2c3"; } -.bi-cloudy-fill::before { content: "\f2c4"; } -.bi-cloudy::before { content: "\f2c5"; } -.bi-code-slash::before { content: "\f2c6"; } -.bi-code-square::before { content: "\f2c7"; } -.bi-code::before { content: "\f2c8"; } -.bi-collection-fill::before { content: "\f2c9"; } -.bi-collection-play-fill::before { content: "\f2ca"; } -.bi-collection-play::before { content: "\f2cb"; } -.bi-collection::before { content: "\f2cc"; } -.bi-columns-gap::before { content: "\f2cd"; } -.bi-columns::before { content: "\f2ce"; } -.bi-command::before { content: "\f2cf"; } -.bi-compass-fill::before { content: "\f2d0"; } -.bi-compass::before { content: "\f2d1"; } -.bi-cone-striped::before { content: "\f2d2"; } -.bi-cone::before { content: "\f2d3"; } -.bi-controller::before { content: "\f2d4"; } -.bi-cpu-fill::before { content: "\f2d5"; } -.bi-cpu::before { content: "\f2d6"; } -.bi-credit-card-2-back-fill::before { content: "\f2d7"; } -.bi-credit-card-2-back::before { content: "\f2d8"; } -.bi-credit-card-2-front-fill::before { content: "\f2d9"; } -.bi-credit-card-2-front::before { content: "\f2da"; } -.bi-credit-card-fill::before { content: "\f2db"; } -.bi-credit-card::before { content: "\f2dc"; } -.bi-crop::before { content: "\f2dd"; } -.bi-cup-fill::before { content: "\f2de"; } -.bi-cup-straw::before { content: "\f2df"; } -.bi-cup::before { content: "\f2e0"; } -.bi-cursor-fill::before { content: "\f2e1"; } -.bi-cursor-text::before { content: "\f2e2"; } -.bi-cursor::before { content: "\f2e3"; } -.bi-dash-circle-dotted::before { content: "\f2e4"; } -.bi-dash-circle-fill::before { content: "\f2e5"; } -.bi-dash-circle::before { content: "\f2e6"; } -.bi-dash-square-dotted::before { content: "\f2e7"; } -.bi-dash-square-fill::before { content: "\f2e8"; } -.bi-dash-square::before { content: "\f2e9"; } -.bi-dash::before { content: "\f2ea"; } -.bi-diagram-2-fill::before { content: "\f2eb"; } -.bi-diagram-2::before { content: "\f2ec"; } -.bi-diagram-3-fill::before { content: "\f2ed"; } -.bi-diagram-3::before { content: "\f2ee"; } -.bi-diamond-fill::before { content: "\f2ef"; } -.bi-diamond-half::before { content: "\f2f0"; } -.bi-diamond::before { content: "\f2f1"; } -.bi-dice-1-fill::before { content: "\f2f2"; } -.bi-dice-1::before { content: "\f2f3"; } -.bi-dice-2-fill::before { content: "\f2f4"; } -.bi-dice-2::before { content: "\f2f5"; } -.bi-dice-3-fill::before { content: "\f2f6"; } -.bi-dice-3::before { content: "\f2f7"; } -.bi-dice-4-fill::before { content: "\f2f8"; } -.bi-dice-4::before { content: "\f2f9"; } -.bi-dice-5-fill::before { content: "\f2fa"; } -.bi-dice-5::before { content: "\f2fb"; } -.bi-dice-6-fill::before { content: "\f2fc"; } -.bi-dice-6::before { content: "\f2fd"; } -.bi-disc-fill::before { content: "\f2fe"; } -.bi-disc::before { content: "\f2ff"; } -.bi-discord::before { content: "\f300"; } -.bi-display-fill::before { content: "\f301"; } -.bi-display::before { content: "\f302"; } -.bi-distribute-horizontal::before { content: "\f303"; } -.bi-distribute-vertical::before { content: "\f304"; } -.bi-door-closed-fill::before { content: "\f305"; } -.bi-door-closed::before { content: "\f306"; } -.bi-door-open-fill::before { content: "\f307"; } -.bi-door-open::before { content: "\f308"; } -.bi-dot::before { content: "\f309"; } -.bi-download::before { content: "\f30a"; } -.bi-droplet-fill::before { content: "\f30b"; } -.bi-droplet-half::before { content: "\f30c"; } -.bi-droplet::before { content: "\f30d"; } -.bi-earbuds::before { content: "\f30e"; } -.bi-easel-fill::before { content: "\f30f"; } -.bi-easel::before { content: "\f310"; } -.bi-egg-fill::before { content: "\f311"; } -.bi-egg-fried::before { content: "\f312"; } -.bi-egg::before { content: "\f313"; } -.bi-eject-fill::before { content: "\f314"; } -.bi-eject::before { content: "\f315"; } -.bi-emoji-angry-fill::before { content: "\f316"; } -.bi-emoji-angry::before { content: "\f317"; } -.bi-emoji-dizzy-fill::before { content: "\f318"; } -.bi-emoji-dizzy::before { content: "\f319"; } -.bi-emoji-expressionless-fill::before { content: "\f31a"; } -.bi-emoji-expressionless::before { content: "\f31b"; } -.bi-emoji-frown-fill::before { content: "\f31c"; } -.bi-emoji-frown::before { content: "\f31d"; } -.bi-emoji-heart-eyes-fill::before { content: "\f31e"; } -.bi-emoji-heart-eyes::before { content: "\f31f"; } -.bi-emoji-laughing-fill::before { content: "\f320"; } -.bi-emoji-laughing::before { content: "\f321"; } -.bi-emoji-neutral-fill::before { content: "\f322"; } -.bi-emoji-neutral::before { content: "\f323"; } -.bi-emoji-smile-fill::before { content: "\f324"; } -.bi-emoji-smile-upside-down-fill::before { content: "\f325"; } -.bi-emoji-smile-upside-down::before { content: "\f326"; } -.bi-emoji-smile::before { content: "\f327"; } -.bi-emoji-sunglasses-fill::before { content: "\f328"; } -.bi-emoji-sunglasses::before { content: "\f329"; } -.bi-emoji-wink-fill::before { content: "\f32a"; } -.bi-emoji-wink::before { content: "\f32b"; } -.bi-envelope-fill::before { content: "\f32c"; } -.bi-envelope-open-fill::before { content: "\f32d"; } -.bi-envelope-open::before { content: "\f32e"; } -.bi-envelope::before { content: "\f32f"; } -.bi-eraser-fill::before { content: "\f330"; } -.bi-eraser::before { content: "\f331"; } -.bi-exclamation-circle-fill::before { content: "\f332"; } -.bi-exclamation-circle::before { content: "\f333"; } -.bi-exclamation-diamond-fill::before { content: "\f334"; } -.bi-exclamation-diamond::before { content: "\f335"; } -.bi-exclamation-octagon-fill::before { content: "\f336"; } -.bi-exclamation-octagon::before { content: "\f337"; } -.bi-exclamation-square-fill::before { content: "\f338"; } -.bi-exclamation-square::before { content: "\f339"; } -.bi-exclamation-triangle-fill::before { content: "\f33a"; } -.bi-exclamation-triangle::before { content: "\f33b"; } -.bi-exclamation::before { content: "\f33c"; } -.bi-exclude::before { content: "\f33d"; } -.bi-eye-fill::before { content: "\f33e"; } -.bi-eye-slash-fill::before { content: "\f33f"; } -.bi-eye-slash::before { content: "\f340"; } -.bi-eye::before { content: "\f341"; } -.bi-eyedropper::before { content: "\f342"; } -.bi-eyeglasses::before { content: "\f343"; } -.bi-facebook::before { content: "\f344"; } -.bi-file-arrow-down-fill::before { content: "\f345"; } -.bi-file-arrow-down::before { content: "\f346"; } -.bi-file-arrow-up-fill::before { content: "\f347"; } -.bi-file-arrow-up::before { content: "\f348"; } -.bi-file-bar-graph-fill::before { content: "\f349"; } -.bi-file-bar-graph::before { content: "\f34a"; } -.bi-file-binary-fill::before { content: "\f34b"; } -.bi-file-binary::before { content: "\f34c"; } -.bi-file-break-fill::before { content: "\f34d"; } -.bi-file-break::before { content: "\f34e"; } -.bi-file-check-fill::before { content: "\f34f"; } -.bi-file-check::before { content: "\f350"; } -.bi-file-code-fill::before { content: "\f351"; } -.bi-file-code::before { content: "\f352"; } -.bi-file-diff-fill::before { content: "\f353"; } -.bi-file-diff::before { content: "\f354"; } -.bi-file-earmark-arrow-down-fill::before { content: "\f355"; } -.bi-file-earmark-arrow-down::before { content: "\f356"; } -.bi-file-earmark-arrow-up-fill::before { content: "\f357"; } -.bi-file-earmark-arrow-up::before { content: "\f358"; } -.bi-file-earmark-bar-graph-fill::before { content: "\f359"; } -.bi-file-earmark-bar-graph::before { content: "\f35a"; } -.bi-file-earmark-binary-fill::before { content: "\f35b"; } -.bi-file-earmark-binary::before { content: "\f35c"; } -.bi-file-earmark-break-fill::before { content: "\f35d"; } -.bi-file-earmark-break::before { content: "\f35e"; } -.bi-file-earmark-check-fill::before { content: "\f35f"; } -.bi-file-earmark-check::before { content: "\f360"; } -.bi-file-earmark-code-fill::before { content: "\f361"; } -.bi-file-earmark-code::before { content: "\f362"; } -.bi-file-earmark-diff-fill::before { content: "\f363"; } -.bi-file-earmark-diff::before { content: "\f364"; } -.bi-file-earmark-easel-fill::before { content: "\f365"; } -.bi-file-earmark-easel::before { content: "\f366"; } -.bi-file-earmark-excel-fill::before { content: "\f367"; } -.bi-file-earmark-excel::before { content: "\f368"; } -.bi-file-earmark-fill::before { content: "\f369"; } -.bi-file-earmark-font-fill::before { content: "\f36a"; } -.bi-file-earmark-font::before { content: "\f36b"; } -.bi-file-earmark-image-fill::before { content: "\f36c"; } -.bi-file-earmark-image::before { content: "\f36d"; } -.bi-file-earmark-lock-fill::before { content: "\f36e"; } -.bi-file-earmark-lock::before { content: "\f36f"; } -.bi-file-earmark-lock2-fill::before { content: "\f370"; } -.bi-file-earmark-lock2::before { content: "\f371"; } -.bi-file-earmark-medical-fill::before { content: "\f372"; } -.bi-file-earmark-medical::before { content: "\f373"; } -.bi-file-earmark-minus-fill::before { content: "\f374"; } -.bi-file-earmark-minus::before { content: "\f375"; } -.bi-file-earmark-music-fill::before { content: "\f376"; } -.bi-file-earmark-music::before { content: "\f377"; } -.bi-file-earmark-person-fill::before { content: "\f378"; } -.bi-file-earmark-person::before { content: "\f379"; } -.bi-file-earmark-play-fill::before { content: "\f37a"; } -.bi-file-earmark-play::before { content: "\f37b"; } -.bi-file-earmark-plus-fill::before { content: "\f37c"; } -.bi-file-earmark-plus::before { content: "\f37d"; } -.bi-file-earmark-post-fill::before { content: "\f37e"; } -.bi-file-earmark-post::before { content: "\f37f"; } -.bi-file-earmark-ppt-fill::before { content: "\f380"; } -.bi-file-earmark-ppt::before { content: "\f381"; } -.bi-file-earmark-richtext-fill::before { content: "\f382"; } -.bi-file-earmark-richtext::before { content: "\f383"; } -.bi-file-earmark-ruled-fill::before { content: "\f384"; } -.bi-file-earmark-ruled::before { content: "\f385"; } -.bi-file-earmark-slides-fill::before { content: "\f386"; } -.bi-file-earmark-slides::before { content: "\f387"; } -.bi-file-earmark-spreadsheet-fill::before { content: "\f388"; } -.bi-file-earmark-spreadsheet::before { content: "\f389"; } -.bi-file-earmark-text-fill::before { content: "\f38a"; } -.bi-file-earmark-text::before { content: "\f38b"; } -.bi-file-earmark-word-fill::before { content: "\f38c"; } -.bi-file-earmark-word::before { content: "\f38d"; } -.bi-file-earmark-x-fill::before { content: "\f38e"; } -.bi-file-earmark-x::before { content: "\f38f"; } -.bi-file-earmark-zip-fill::before { content: "\f390"; } -.bi-file-earmark-zip::before { content: "\f391"; } -.bi-file-earmark::before { content: "\f392"; } -.bi-file-easel-fill::before { content: "\f393"; } -.bi-file-easel::before { content: "\f394"; } -.bi-file-excel-fill::before { content: "\f395"; } -.bi-file-excel::before { content: "\f396"; } -.bi-file-fill::before { content: "\f397"; } -.bi-file-font-fill::before { content: "\f398"; } -.bi-file-font::before { content: "\f399"; } -.bi-file-image-fill::before { content: "\f39a"; } -.bi-file-image::before { content: "\f39b"; } -.bi-file-lock-fill::before { content: "\f39c"; } -.bi-file-lock::before { content: "\f39d"; } -.bi-file-lock2-fill::before { content: "\f39e"; } -.bi-file-lock2::before { content: "\f39f"; } -.bi-file-medical-fill::before { content: "\f3a0"; } -.bi-file-medical::before { content: "\f3a1"; } -.bi-file-minus-fill::before { content: "\f3a2"; } -.bi-file-minus::before { content: "\f3a3"; } -.bi-file-music-fill::before { content: "\f3a4"; } -.bi-file-music::before { content: "\f3a5"; } -.bi-file-person-fill::before { content: "\f3a6"; } -.bi-file-person::before { content: "\f3a7"; } -.bi-file-play-fill::before { content: "\f3a8"; } -.bi-file-play::before { content: "\f3a9"; } -.bi-file-plus-fill::before { content: "\f3aa"; } -.bi-file-plus::before { content: "\f3ab"; } -.bi-file-post-fill::before { content: "\f3ac"; } -.bi-file-post::before { content: "\f3ad"; } -.bi-file-ppt-fill::before { content: "\f3ae"; } -.bi-file-ppt::before { content: "\f3af"; } -.bi-file-richtext-fill::before { content: "\f3b0"; } -.bi-file-richtext::before { content: "\f3b1"; } -.bi-file-ruled-fill::before { content: "\f3b2"; } -.bi-file-ruled::before { content: "\f3b3"; } -.bi-file-slides-fill::before { content: "\f3b4"; } -.bi-file-slides::before { content: "\f3b5"; } -.bi-file-spreadsheet-fill::before { content: "\f3b6"; } -.bi-file-spreadsheet::before { content: "\f3b7"; } -.bi-file-text-fill::before { content: "\f3b8"; } -.bi-file-text::before { content: "\f3b9"; } -.bi-file-word-fill::before { content: "\f3ba"; } -.bi-file-word::before { content: "\f3bb"; } -.bi-file-x-fill::before { content: "\f3bc"; } -.bi-file-x::before { content: "\f3bd"; } -.bi-file-zip-fill::before { content: "\f3be"; } -.bi-file-zip::before { content: "\f3bf"; } -.bi-file::before { content: "\f3c0"; } -.bi-files-alt::before { content: "\f3c1"; } -.bi-files::before { content: "\f3c2"; } -.bi-film::before { content: "\f3c3"; } -.bi-filter-circle-fill::before { content: "\f3c4"; } -.bi-filter-circle::before { content: "\f3c5"; } -.bi-filter-left::before { content: "\f3c6"; } -.bi-filter-right::before { content: "\f3c7"; } -.bi-filter-square-fill::before { content: "\f3c8"; } -.bi-filter-square::before { content: "\f3c9"; } -.bi-filter::before { content: "\f3ca"; } -.bi-flag-fill::before { content: "\f3cb"; } -.bi-flag::before { content: "\f3cc"; } -.bi-flower1::before { content: "\f3cd"; } -.bi-flower2::before { content: "\f3ce"; } -.bi-flower3::before { content: "\f3cf"; } -.bi-folder-check::before { content: "\f3d0"; } -.bi-folder-fill::before { content: "\f3d1"; } -.bi-folder-minus::before { content: "\f3d2"; } -.bi-folder-plus::before { content: "\f3d3"; } -.bi-folder-symlink-fill::before { content: "\f3d4"; } -.bi-folder-symlink::before { content: "\f3d5"; } -.bi-folder-x::before { content: "\f3d6"; } -.bi-folder::before { content: "\f3d7"; } -.bi-folder2-open::before { content: "\f3d8"; } -.bi-folder2::before { content: "\f3d9"; } -.bi-fonts::before { content: "\f3da"; } -.bi-forward-fill::before { content: "\f3db"; } -.bi-forward::before { content: "\f3dc"; } -.bi-front::before { content: "\f3dd"; } -.bi-fullscreen-exit::before { content: "\f3de"; } -.bi-fullscreen::before { content: "\f3df"; } -.bi-funnel-fill::before { content: "\f3e0"; } -.bi-funnel::before { content: "\f3e1"; } -.bi-gear-fill::before { content: "\f3e2"; } -.bi-gear-wide-connected::before { content: "\f3e3"; } -.bi-gear-wide::before { content: "\f3e4"; } -.bi-gear::before { content: "\f3e5"; } -.bi-gem::before { content: "\f3e6"; } -.bi-geo-alt-fill::before { content: "\f3e7"; } -.bi-geo-alt::before { content: "\f3e8"; } -.bi-geo-fill::before { content: "\f3e9"; } -.bi-geo::before { content: "\f3ea"; } -.bi-gift-fill::before { content: "\f3eb"; } -.bi-gift::before { content: "\f3ec"; } -.bi-github::before { content: "\f3ed"; } -.bi-globe::before { content: "\f3ee"; } -.bi-globe2::before { content: "\f3ef"; } -.bi-google::before { content: "\f3f0"; } -.bi-graph-down::before { content: "\f3f1"; } -.bi-graph-up::before { content: "\f3f2"; } -.bi-grid-1x2-fill::before { content: "\f3f3"; } -.bi-grid-1x2::before { content: "\f3f4"; } -.bi-grid-3x2-gap-fill::before { content: "\f3f5"; } -.bi-grid-3x2-gap::before { content: "\f3f6"; } -.bi-grid-3x2::before { content: "\f3f7"; } -.bi-grid-3x3-gap-fill::before { content: "\f3f8"; } -.bi-grid-3x3-gap::before { content: "\f3f9"; } -.bi-grid-3x3::before { content: "\f3fa"; } -.bi-grid-fill::before { content: "\f3fb"; } -.bi-grid::before { content: "\f3fc"; } -.bi-grip-horizontal::before { content: "\f3fd"; } -.bi-grip-vertical::before { content: "\f3fe"; } -.bi-hammer::before { content: "\f3ff"; } -.bi-hand-index-fill::before { content: "\f400"; } -.bi-hand-index-thumb-fill::before { content: "\f401"; } -.bi-hand-index-thumb::before { content: "\f402"; } -.bi-hand-index::before { content: "\f403"; } -.bi-hand-thumbs-down-fill::before { content: "\f404"; } -.bi-hand-thumbs-down::before { content: "\f405"; } -.bi-hand-thumbs-up-fill::before { content: "\f406"; } -.bi-hand-thumbs-up::before { content: "\f407"; } -.bi-handbag-fill::before { content: "\f408"; } -.bi-handbag::before { content: "\f409"; } -.bi-hash::before { content: "\f40a"; } -.bi-hdd-fill::before { content: "\f40b"; } -.bi-hdd-network-fill::before { content: "\f40c"; } -.bi-hdd-network::before { content: "\f40d"; } -.bi-hdd-rack-fill::before { content: "\f40e"; } -.bi-hdd-rack::before { content: "\f40f"; } -.bi-hdd-stack-fill::before { content: "\f410"; } -.bi-hdd-stack::before { content: "\f411"; } -.bi-hdd::before { content: "\f412"; } -.bi-headphones::before { content: "\f413"; } -.bi-headset::before { content: "\f414"; } -.bi-heart-fill::before { content: "\f415"; } -.bi-heart-half::before { content: "\f416"; } -.bi-heart::before { content: "\f417"; } -.bi-heptagon-fill::before { content: "\f418"; } -.bi-heptagon-half::before { content: "\f419"; } -.bi-heptagon::before { content: "\f41a"; } -.bi-hexagon-fill::before { content: "\f41b"; } -.bi-hexagon-half::before { content: "\f41c"; } -.bi-hexagon::before { content: "\f41d"; } -.bi-hourglass-bottom::before { content: "\f41e"; } -.bi-hourglass-split::before { content: "\f41f"; } -.bi-hourglass-top::before { content: "\f420"; } -.bi-hourglass::before { content: "\f421"; } -.bi-house-door-fill::before { content: "\f422"; } -.bi-house-door::before { content: "\f423"; } -.bi-house-fill::before { content: "\f424"; } -.bi-house::before { content: "\f425"; } -.bi-hr::before { content: "\f426"; } -.bi-hurricane::before { content: "\f427"; } -.bi-image-alt::before { content: "\f428"; } -.bi-image-fill::before { content: "\f429"; } -.bi-image::before { content: "\f42a"; } -.bi-images::before { content: "\f42b"; } -.bi-inbox-fill::before { content: "\f42c"; } -.bi-inbox::before { content: "\f42d"; } -.bi-inboxes-fill::before { content: "\f42e"; } -.bi-inboxes::before { content: "\f42f"; } -.bi-info-circle-fill::before { content: "\f430"; } -.bi-info-circle::before { content: "\f431"; } -.bi-info-square-fill::before { content: "\f432"; } -.bi-info-square::before { content: "\f433"; } -.bi-info::before { content: "\f434"; } -.bi-input-cursor-text::before { content: "\f435"; } -.bi-input-cursor::before { content: "\f436"; } -.bi-instagram::before { content: "\f437"; } -.bi-intersect::before { content: "\f438"; } -.bi-journal-album::before { content: "\f439"; } -.bi-journal-arrow-down::before { content: "\f43a"; } -.bi-journal-arrow-up::before { content: "\f43b"; } -.bi-journal-bookmark-fill::before { content: "\f43c"; } -.bi-journal-bookmark::before { content: "\f43d"; } -.bi-journal-check::before { content: "\f43e"; } -.bi-journal-code::before { content: "\f43f"; } -.bi-journal-medical::before { content: "\f440"; } -.bi-journal-minus::before { content: "\f441"; } -.bi-journal-plus::before { content: "\f442"; } -.bi-journal-richtext::before { content: "\f443"; } -.bi-journal-text::before { content: "\f444"; } -.bi-journal-x::before { content: "\f445"; } -.bi-journal::before { content: "\f446"; } -.bi-journals::before { content: "\f447"; } -.bi-joystick::before { content: "\f448"; } -.bi-justify-left::before { content: "\f449"; } -.bi-justify-right::before { content: "\f44a"; } -.bi-justify::before { content: "\f44b"; } -.bi-kanban-fill::before { content: "\f44c"; } -.bi-kanban::before { content: "\f44d"; } -.bi-key-fill::before { content: "\f44e"; } -.bi-key::before { content: "\f44f"; } -.bi-keyboard-fill::before { content: "\f450"; } -.bi-keyboard::before { content: "\f451"; } -.bi-ladder::before { content: "\f452"; } -.bi-lamp-fill::before { content: "\f453"; } -.bi-lamp::before { content: "\f454"; } -.bi-laptop-fill::before { content: "\f455"; } -.bi-laptop::before { content: "\f456"; } -.bi-layer-backward::before { content: "\f457"; } -.bi-layer-forward::before { content: "\f458"; } -.bi-layers-fill::before { content: "\f459"; } -.bi-layers-half::before { content: "\f45a"; } -.bi-layers::before { content: "\f45b"; } -.bi-layout-sidebar-inset-reverse::before { content: "\f45c"; } -.bi-layout-sidebar-inset::before { content: "\f45d"; } -.bi-layout-sidebar-reverse::before { content: "\f45e"; } -.bi-layout-sidebar::before { content: "\f45f"; } -.bi-layout-split::before { content: "\f460"; } -.bi-layout-text-sidebar-reverse::before { content: "\f461"; } -.bi-layout-text-sidebar::before { content: "\f462"; } -.bi-layout-text-window-reverse::before { content: "\f463"; } -.bi-layout-text-window::before { content: "\f464"; } -.bi-layout-three-columns::before { content: "\f465"; } -.bi-layout-wtf::before { content: "\f466"; } -.bi-life-preserver::before { content: "\f467"; } -.bi-lightbulb-fill::before { content: "\f468"; } -.bi-lightbulb-off-fill::before { content: "\f469"; } -.bi-lightbulb-off::before { content: "\f46a"; } -.bi-lightbulb::before { content: "\f46b"; } -.bi-lightning-charge-fill::before { content: "\f46c"; } -.bi-lightning-charge::before { content: "\f46d"; } -.bi-lightning-fill::before { content: "\f46e"; } -.bi-lightning::before { content: "\f46f"; } -.bi-link-45deg::before { content: "\f470"; } -.bi-link::before { content: "\f471"; } -.bi-linkedin::before { content: "\f472"; } -.bi-list-check::before { content: "\f473"; } -.bi-list-nested::before { content: "\f474"; } -.bi-list-ol::before { content: "\f475"; } -.bi-list-stars::before { content: "\f476"; } -.bi-list-task::before { content: "\f477"; } -.bi-list-ul::before { content: "\f478"; } -.bi-list::before { content: "\f479"; } -.bi-lock-fill::before { content: "\f47a"; } -.bi-lock::before { content: "\f47b"; } -.bi-mailbox::before { content: "\f47c"; } -.bi-mailbox2::before { content: "\f47d"; } -.bi-map-fill::before { content: "\f47e"; } -.bi-map::before { content: "\f47f"; } -.bi-markdown-fill::before { content: "\f480"; } -.bi-markdown::before { content: "\f481"; } -.bi-mask::before { content: "\f482"; } -.bi-megaphone-fill::before { content: "\f483"; } -.bi-megaphone::before { content: "\f484"; } -.bi-menu-app-fill::before { content: "\f485"; } -.bi-menu-app::before { content: "\f486"; } -.bi-menu-button-fill::before { content: "\f487"; } -.bi-menu-button-wide-fill::before { content: "\f488"; } -.bi-menu-button-wide::before { content: "\f489"; } -.bi-menu-button::before { content: "\f48a"; } -.bi-menu-down::before { content: "\f48b"; } -.bi-menu-up::before { content: "\f48c"; } -.bi-mic-fill::before { content: "\f48d"; } -.bi-mic-mute-fill::before { content: "\f48e"; } -.bi-mic-mute::before { content: "\f48f"; } -.bi-mic::before { content: "\f490"; } -.bi-minecart-loaded::before { content: "\f491"; } -.bi-minecart::before { content: "\f492"; } -.bi-moisture::before { content: "\f493"; } -.bi-moon-fill::before { content: "\f494"; } -.bi-moon-stars-fill::before { content: "\f495"; } -.bi-moon-stars::before { content: "\f496"; } -.bi-moon::before { content: "\f497"; } -.bi-mouse-fill::before { content: "\f498"; } -.bi-mouse::before { content: "\f499"; } -.bi-mouse2-fill::before { content: "\f49a"; } -.bi-mouse2::before { content: "\f49b"; } -.bi-mouse3-fill::before { content: "\f49c"; } -.bi-mouse3::before { content: "\f49d"; } -.bi-music-note-beamed::before { content: "\f49e"; } -.bi-music-note-list::before { content: "\f49f"; } -.bi-music-note::before { content: "\f4a0"; } -.bi-music-player-fill::before { content: "\f4a1"; } -.bi-music-player::before { content: "\f4a2"; } -.bi-newspaper::before { content: "\f4a3"; } -.bi-node-minus-fill::before { content: "\f4a4"; } -.bi-node-minus::before { content: "\f4a5"; } -.bi-node-plus-fill::before { content: "\f4a6"; } -.bi-node-plus::before { content: "\f4a7"; } -.bi-nut-fill::before { content: "\f4a8"; } -.bi-nut::before { content: "\f4a9"; } -.bi-octagon-fill::before { content: "\f4aa"; } -.bi-octagon-half::before { content: "\f4ab"; } -.bi-octagon::before { content: "\f4ac"; } -.bi-option::before { content: "\f4ad"; } -.bi-outlet::before { content: "\f4ae"; } -.bi-paint-bucket::before { content: "\f4af"; } -.bi-palette-fill::before { content: "\f4b0"; } -.bi-palette::before { content: "\f4b1"; } -.bi-palette2::before { content: "\f4b2"; } -.bi-paperclip::before { content: "\f4b3"; } -.bi-paragraph::before { content: "\f4b4"; } -.bi-patch-check-fill::before { content: "\f4b5"; } -.bi-patch-check::before { content: "\f4b6"; } -.bi-patch-exclamation-fill::before { content: "\f4b7"; } -.bi-patch-exclamation::before { content: "\f4b8"; } -.bi-patch-minus-fill::before { content: "\f4b9"; } -.bi-patch-minus::before { content: "\f4ba"; } -.bi-patch-plus-fill::before { content: "\f4bb"; } -.bi-patch-plus::before { content: "\f4bc"; } -.bi-patch-question-fill::before { content: "\f4bd"; } -.bi-patch-question::before { content: "\f4be"; } -.bi-pause-btn-fill::before { content: "\f4bf"; } -.bi-pause-btn::before { content: "\f4c0"; } -.bi-pause-circle-fill::before { content: "\f4c1"; } -.bi-pause-circle::before { content: "\f4c2"; } -.bi-pause-fill::before { content: "\f4c3"; } -.bi-pause::before { content: "\f4c4"; } -.bi-peace-fill::before { content: "\f4c5"; } -.bi-peace::before { content: "\f4c6"; } -.bi-pen-fill::before { content: "\f4c7"; } -.bi-pen::before { content: "\f4c8"; } -.bi-pencil-fill::before { content: "\f4c9"; } -.bi-pencil-square::before { content: "\f4ca"; } -.bi-pencil::before { content: "\f4cb"; } -.bi-pentagon-fill::before { content: "\f4cc"; } -.bi-pentagon-half::before { content: "\f4cd"; } -.bi-pentagon::before { content: "\f4ce"; } -.bi-people-fill::before { content: "\f4cf"; } -.bi-people::before { content: "\f4d0"; } -.bi-percent::before { content: "\f4d1"; } -.bi-person-badge-fill::before { content: "\f4d2"; } -.bi-person-badge::before { content: "\f4d3"; } -.bi-person-bounding-box::before { content: "\f4d4"; } -.bi-person-check-fill::before { content: "\f4d5"; } -.bi-person-check::before { content: "\f4d6"; } -.bi-person-circle::before { content: "\f4d7"; } -.bi-person-dash-fill::before { content: "\f4d8"; } -.bi-person-dash::before { content: "\f4d9"; } -.bi-person-fill::before { content: "\f4da"; } -.bi-person-lines-fill::before { content: "\f4db"; } -.bi-person-plus-fill::before { content: "\f4dc"; } -.bi-person-plus::before { content: "\f4dd"; } -.bi-person-square::before { content: "\f4de"; } -.bi-person-x-fill::before { content: "\f4df"; } -.bi-person-x::before { content: "\f4e0"; } -.bi-person::before { content: "\f4e1"; } -.bi-phone-fill::before { content: "\f4e2"; } -.bi-phone-landscape-fill::before { content: "\f4e3"; } -.bi-phone-landscape::before { content: "\f4e4"; } -.bi-phone-vibrate-fill::before { content: "\f4e5"; } -.bi-phone-vibrate::before { content: "\f4e6"; } -.bi-phone::before { content: "\f4e7"; } -.bi-pie-chart-fill::before { content: "\f4e8"; } -.bi-pie-chart::before { content: "\f4e9"; } -.bi-pin-angle-fill::before { content: "\f4ea"; } -.bi-pin-angle::before { content: "\f4eb"; } -.bi-pin-fill::before { content: "\f4ec"; } -.bi-pin::before { content: "\f4ed"; } -.bi-pip-fill::before { content: "\f4ee"; } -.bi-pip::before { content: "\f4ef"; } -.bi-play-btn-fill::before { content: "\f4f0"; } -.bi-play-btn::before { content: "\f4f1"; } -.bi-play-circle-fill::before { content: "\f4f2"; } -.bi-play-circle::before { content: "\f4f3"; } -.bi-play-fill::before { content: "\f4f4"; } -.bi-play::before { content: "\f4f5"; } -.bi-plug-fill::before { content: "\f4f6"; } -.bi-plug::before { content: "\f4f7"; } -.bi-plus-circle-dotted::before { content: "\f4f8"; } -.bi-plus-circle-fill::before { content: "\f4f9"; } -.bi-plus-circle::before { content: "\f4fa"; } -.bi-plus-square-dotted::before { content: "\f4fb"; } -.bi-plus-square-fill::before { content: "\f4fc"; } -.bi-plus-square::before { content: "\f4fd"; } -.bi-plus::before { content: "\f4fe"; } -.bi-power::before { content: "\f4ff"; } -.bi-printer-fill::before { content: "\f500"; } -.bi-printer::before { content: "\f501"; } -.bi-puzzle-fill::before { content: "\f502"; } -.bi-puzzle::before { content: "\f503"; } -.bi-question-circle-fill::before { content: "\f504"; } -.bi-question-circle::before { content: "\f505"; } -.bi-question-diamond-fill::before { content: "\f506"; } -.bi-question-diamond::before { content: "\f507"; } -.bi-question-octagon-fill::before { content: "\f508"; } -.bi-question-octagon::before { content: "\f509"; } -.bi-question-square-fill::before { content: "\f50a"; } -.bi-question-square::before { content: "\f50b"; } -.bi-question::before { content: "\f50c"; } -.bi-rainbow::before { content: "\f50d"; } -.bi-receipt-cutoff::before { content: "\f50e"; } -.bi-receipt::before { content: "\f50f"; } -.bi-reception-0::before { content: "\f510"; } -.bi-reception-1::before { content: "\f511"; } -.bi-reception-2::before { content: "\f512"; } -.bi-reception-3::before { content: "\f513"; } -.bi-reception-4::before { content: "\f514"; } -.bi-record-btn-fill::before { content: "\f515"; } -.bi-record-btn::before { content: "\f516"; } -.bi-record-circle-fill::before { content: "\f517"; } -.bi-record-circle::before { content: "\f518"; } -.bi-record-fill::before { content: "\f519"; } -.bi-record::before { content: "\f51a"; } -.bi-record2-fill::before { content: "\f51b"; } -.bi-record2::before { content: "\f51c"; } -.bi-reply-all-fill::before { content: "\f51d"; } -.bi-reply-all::before { content: "\f51e"; } -.bi-reply-fill::before { content: "\f51f"; } -.bi-reply::before { content: "\f520"; } -.bi-rss-fill::before { content: "\f521"; } -.bi-rss::before { content: "\f522"; } -.bi-rulers::before { content: "\f523"; } -.bi-save-fill::before { content: "\f524"; } -.bi-save::before { content: "\f525"; } -.bi-save2-fill::before { content: "\f526"; } -.bi-save2::before { content: "\f527"; } -.bi-scissors::before { content: "\f528"; } -.bi-screwdriver::before { content: "\f529"; } -.bi-search::before { content: "\f52a"; } -.bi-segmented-nav::before { content: "\f52b"; } -.bi-server::before { content: "\f52c"; } -.bi-share-fill::before { content: "\f52d"; } -.bi-share::before { content: "\f52e"; } -.bi-shield-check::before { content: "\f52f"; } -.bi-shield-exclamation::before { content: "\f530"; } -.bi-shield-fill-check::before { content: "\f531"; } -.bi-shield-fill-exclamation::before { content: "\f532"; } -.bi-shield-fill-minus::before { content: "\f533"; } -.bi-shield-fill-plus::before { content: "\f534"; } -.bi-shield-fill-x::before { content: "\f535"; } -.bi-shield-fill::before { content: "\f536"; } -.bi-shield-lock-fill::before { content: "\f537"; } -.bi-shield-lock::before { content: "\f538"; } -.bi-shield-minus::before { content: "\f539"; } -.bi-shield-plus::before { content: "\f53a"; } -.bi-shield-shaded::before { content: "\f53b"; } -.bi-shield-slash-fill::before { content: "\f53c"; } -.bi-shield-slash::before { content: "\f53d"; } -.bi-shield-x::before { content: "\f53e"; } -.bi-shield::before { content: "\f53f"; } -.bi-shift-fill::before { content: "\f540"; } -.bi-shift::before { content: "\f541"; } -.bi-shop-window::before { content: "\f542"; } -.bi-shop::before { content: "\f543"; } -.bi-shuffle::before { content: "\f544"; } -.bi-signpost-2-fill::before { content: "\f545"; } -.bi-signpost-2::before { content: "\f546"; } -.bi-signpost-fill::before { content: "\f547"; } -.bi-signpost-split-fill::before { content: "\f548"; } -.bi-signpost-split::before { content: "\f549"; } -.bi-signpost::before { content: "\f54a"; } -.bi-sim-fill::before { content: "\f54b"; } -.bi-sim::before { content: "\f54c"; } -.bi-skip-backward-btn-fill::before { content: "\f54d"; } -.bi-skip-backward-btn::before { content: "\f54e"; } -.bi-skip-backward-circle-fill::before { content: "\f54f"; } -.bi-skip-backward-circle::before { content: "\f550"; } -.bi-skip-backward-fill::before { content: "\f551"; } -.bi-skip-backward::before { content: "\f552"; } -.bi-skip-end-btn-fill::before { content: "\f553"; } -.bi-skip-end-btn::before { content: "\f554"; } -.bi-skip-end-circle-fill::before { content: "\f555"; } -.bi-skip-end-circle::before { content: "\f556"; } -.bi-skip-end-fill::before { content: "\f557"; } -.bi-skip-end::before { content: "\f558"; } -.bi-skip-forward-btn-fill::before { content: "\f559"; } -.bi-skip-forward-btn::before { content: "\f55a"; } -.bi-skip-forward-circle-fill::before { content: "\f55b"; } -.bi-skip-forward-circle::before { content: "\f55c"; } -.bi-skip-forward-fill::before { content: "\f55d"; } -.bi-skip-forward::before { content: "\f55e"; } -.bi-skip-start-btn-fill::before { content: "\f55f"; } -.bi-skip-start-btn::before { content: "\f560"; } -.bi-skip-start-circle-fill::before { content: "\f561"; } -.bi-skip-start-circle::before { content: "\f562"; } -.bi-skip-start-fill::before { content: "\f563"; } -.bi-skip-start::before { content: "\f564"; } -.bi-slack::before { content: "\f565"; } -.bi-slash-circle-fill::before { content: "\f566"; } -.bi-slash-circle::before { content: "\f567"; } -.bi-slash-square-fill::before { content: "\f568"; } -.bi-slash-square::before { content: "\f569"; } -.bi-slash::before { content: "\f56a"; } -.bi-sliders::before { content: "\f56b"; } -.bi-smartwatch::before { content: "\f56c"; } -.bi-snow::before { content: "\f56d"; } -.bi-snow2::before { content: "\f56e"; } -.bi-snow3::before { content: "\f56f"; } -.bi-sort-alpha-down-alt::before { content: "\f570"; } -.bi-sort-alpha-down::before { content: "\f571"; } -.bi-sort-alpha-up-alt::before { content: "\f572"; } -.bi-sort-alpha-up::before { content: "\f573"; } -.bi-sort-down-alt::before { content: "\f574"; } -.bi-sort-down::before { content: "\f575"; } -.bi-sort-numeric-down-alt::before { content: "\f576"; } -.bi-sort-numeric-down::before { content: "\f577"; } -.bi-sort-numeric-up-alt::before { content: "\f578"; } -.bi-sort-numeric-up::before { content: "\f579"; } -.bi-sort-up-alt::before { content: "\f57a"; } -.bi-sort-up::before { content: "\f57b"; } -.bi-soundwave::before { content: "\f57c"; } -.bi-speaker-fill::before { content: "\f57d"; } -.bi-speaker::before { content: "\f57e"; } -.bi-speedometer::before { content: "\f57f"; } -.bi-speedometer2::before { content: "\f580"; } -.bi-spellcheck::before { content: "\f581"; } -.bi-square-fill::before { content: "\f582"; } -.bi-square-half::before { content: "\f583"; } -.bi-square::before { content: "\f584"; } -.bi-stack::before { content: "\f585"; } -.bi-star-fill::before { content: "\f586"; } -.bi-star-half::before { content: "\f587"; } -.bi-star::before { content: "\f588"; } -.bi-stars::before { content: "\f589"; } -.bi-stickies-fill::before { content: "\f58a"; } -.bi-stickies::before { content: "\f58b"; } -.bi-sticky-fill::before { content: "\f58c"; } -.bi-sticky::before { content: "\f58d"; } -.bi-stop-btn-fill::before { content: "\f58e"; } -.bi-stop-btn::before { content: "\f58f"; } -.bi-stop-circle-fill::before { content: "\f590"; } -.bi-stop-circle::before { content: "\f591"; } -.bi-stop-fill::before { content: "\f592"; } -.bi-stop::before { content: "\f593"; } -.bi-stoplights-fill::before { content: "\f594"; } -.bi-stoplights::before { content: "\f595"; } -.bi-stopwatch-fill::before { content: "\f596"; } -.bi-stopwatch::before { content: "\f597"; } -.bi-subtract::before { content: "\f598"; } -.bi-suit-club-fill::before { content: "\f599"; } -.bi-suit-club::before { content: "\f59a"; } -.bi-suit-diamond-fill::before { content: "\f59b"; } -.bi-suit-diamond::before { content: "\f59c"; } -.bi-suit-heart-fill::before { content: "\f59d"; } -.bi-suit-heart::before { content: "\f59e"; } -.bi-suit-spade-fill::before { content: "\f59f"; } -.bi-suit-spade::before { content: "\f5a0"; } -.bi-sun-fill::before { content: "\f5a1"; } -.bi-sun::before { content: "\f5a2"; } -.bi-sunglasses::before { content: "\f5a3"; } -.bi-sunrise-fill::before { content: "\f5a4"; } -.bi-sunrise::before { content: "\f5a5"; } -.bi-sunset-fill::before { content: "\f5a6"; } -.bi-sunset::before { content: "\f5a7"; } -.bi-symmetry-horizontal::before { content: "\f5a8"; } -.bi-symmetry-vertical::before { content: "\f5a9"; } -.bi-table::before { content: "\f5aa"; } -.bi-tablet-fill::before { content: "\f5ab"; } -.bi-tablet-landscape-fill::before { content: "\f5ac"; } -.bi-tablet-landscape::before { content: "\f5ad"; } -.bi-tablet::before { content: "\f5ae"; } -.bi-tag-fill::before { content: "\f5af"; } -.bi-tag::before { content: "\f5b0"; } -.bi-tags-fill::before { content: "\f5b1"; } -.bi-tags::before { content: "\f5b2"; } -.bi-telegram::before { content: "\f5b3"; } -.bi-telephone-fill::before { content: "\f5b4"; } -.bi-telephone-forward-fill::before { content: "\f5b5"; } -.bi-telephone-forward::before { content: "\f5b6"; } -.bi-telephone-inbound-fill::before { content: "\f5b7"; } -.bi-telephone-inbound::before { content: "\f5b8"; } -.bi-telephone-minus-fill::before { content: "\f5b9"; } -.bi-telephone-minus::before { content: "\f5ba"; } -.bi-telephone-outbound-fill::before { content: "\f5bb"; } -.bi-telephone-outbound::before { content: "\f5bc"; } -.bi-telephone-plus-fill::before { content: "\f5bd"; } -.bi-telephone-plus::before { content: "\f5be"; } -.bi-telephone-x-fill::before { content: "\f5bf"; } -.bi-telephone-x::before { content: "\f5c0"; } -.bi-telephone::before { content: "\f5c1"; } -.bi-terminal-fill::before { content: "\f5c2"; } -.bi-terminal::before { content: "\f5c3"; } -.bi-text-center::before { content: "\f5c4"; } -.bi-text-indent-left::before { content: "\f5c5"; } -.bi-text-indent-right::before { content: "\f5c6"; } -.bi-text-left::before { content: "\f5c7"; } -.bi-text-paragraph::before { content: "\f5c8"; } -.bi-text-right::before { content: "\f5c9"; } -.bi-textarea-resize::before { content: "\f5ca"; } -.bi-textarea-t::before { content: "\f5cb"; } -.bi-textarea::before { content: "\f5cc"; } -.bi-thermometer-half::before { content: "\f5cd"; } -.bi-thermometer-high::before { content: "\f5ce"; } -.bi-thermometer-low::before { content: "\f5cf"; } -.bi-thermometer-snow::before { content: "\f5d0"; } -.bi-thermometer-sun::before { content: "\f5d1"; } -.bi-thermometer::before { content: "\f5d2"; } -.bi-three-dots-vertical::before { content: "\f5d3"; } -.bi-three-dots::before { content: "\f5d4"; } -.bi-toggle-off::before { content: "\f5d5"; } -.bi-toggle-on::before { content: "\f5d6"; } -.bi-toggle2-off::before { content: "\f5d7"; } -.bi-toggle2-on::before { content: "\f5d8"; } -.bi-toggles::before { content: "\f5d9"; } -.bi-toggles2::before { content: "\f5da"; } -.bi-tools::before { content: "\f5db"; } -.bi-tornado::before { content: "\f5dc"; } -.bi-trash-fill::before { content: "\f5dd"; } -.bi-trash::before { content: "\f5de"; } -.bi-trash2-fill::before { content: "\f5df"; } -.bi-trash2::before { content: "\f5e0"; } -.bi-tree-fill::before { content: "\f5e1"; } -.bi-tree::before { content: "\f5e2"; } -.bi-triangle-fill::before { content: "\f5e3"; } -.bi-triangle-half::before { content: "\f5e4"; } -.bi-triangle::before { content: "\f5e5"; } -.bi-trophy-fill::before { content: "\f5e6"; } -.bi-trophy::before { content: "\f5e7"; } -.bi-tropical-storm::before { content: "\f5e8"; } -.bi-truck-flatbed::before { content: "\f5e9"; } -.bi-truck::before { content: "\f5ea"; } -.bi-tsunami::before { content: "\f5eb"; } -.bi-tv-fill::before { content: "\f5ec"; } -.bi-tv::before { content: "\f5ed"; } -.bi-twitch::before { content: "\f5ee"; } -.bi-twitter::before { content: "\f5ef"; } -.bi-type-bold::before { content: "\f5f0"; } -.bi-type-h1::before { content: "\f5f1"; } -.bi-type-h2::before { content: "\f5f2"; } -.bi-type-h3::before { content: "\f5f3"; } -.bi-type-italic::before { content: "\f5f4"; } -.bi-type-strikethrough::before { content: "\f5f5"; } -.bi-type-underline::before { content: "\f5f6"; } -.bi-type::before { content: "\f5f7"; } -.bi-ui-checks-grid::before { content: "\f5f8"; } -.bi-ui-checks::before { content: "\f5f9"; } -.bi-ui-radios-grid::before { content: "\f5fa"; } -.bi-ui-radios::before { content: "\f5fb"; } -.bi-umbrella-fill::before { content: "\f5fc"; } -.bi-umbrella::before { content: "\f5fd"; } -.bi-union::before { content: "\f5fe"; } -.bi-unlock-fill::before { content: "\f5ff"; } -.bi-unlock::before { content: "\f600"; } -.bi-upc-scan::before { content: "\f601"; } -.bi-upc::before { content: "\f602"; } -.bi-upload::before { content: "\f603"; } -.bi-vector-pen::before { content: "\f604"; } -.bi-view-list::before { content: "\f605"; } -.bi-view-stacked::before { content: "\f606"; } -.bi-vinyl-fill::before { content: "\f607"; } -.bi-vinyl::before { content: "\f608"; } -.bi-voicemail::before { content: "\f609"; } -.bi-volume-down-fill::before { content: "\f60a"; } -.bi-volume-down::before { content: "\f60b"; } -.bi-volume-mute-fill::before { content: "\f60c"; } -.bi-volume-mute::before { content: "\f60d"; } -.bi-volume-off-fill::before { content: "\f60e"; } -.bi-volume-off::before { content: "\f60f"; } -.bi-volume-up-fill::before { content: "\f610"; } -.bi-volume-up::before { content: "\f611"; } -.bi-vr::before { content: "\f612"; } -.bi-wallet-fill::before { content: "\f613"; } -.bi-wallet::before { content: "\f614"; } -.bi-wallet2::before { content: "\f615"; } -.bi-watch::before { content: "\f616"; } -.bi-water::before { content: "\f617"; } -.bi-whatsapp::before { content: "\f618"; } -.bi-wifi-1::before { content: "\f619"; } -.bi-wifi-2::before { content: "\f61a"; } -.bi-wifi-off::before { content: "\f61b"; } -.bi-wifi::before { content: "\f61c"; } -.bi-wind::before { content: "\f61d"; } -.bi-window-dock::before { content: "\f61e"; } -.bi-window-sidebar::before { content: "\f61f"; } -.bi-window::before { content: "\f620"; } -.bi-wrench::before { content: "\f621"; } -.bi-x-circle-fill::before { content: "\f622"; } -.bi-x-circle::before { content: "\f623"; } -.bi-x-diamond-fill::before { content: "\f624"; } -.bi-x-diamond::before { content: "\f625"; } -.bi-x-octagon-fill::before { content: "\f626"; } -.bi-x-octagon::before { content: "\f627"; } -.bi-x-square-fill::before { content: "\f628"; } -.bi-x-square::before { content: "\f629"; } -.bi-x::before { content: "\f62a"; } -.bi-youtube::before { content: "\f62b"; } -.bi-zoom-in::before { content: "\f62c"; } -.bi-zoom-out::before { content: "\f62d"; } -.bi-bank::before { content: "\f62e"; } -.bi-bank2::before { content: "\f62f"; } -.bi-bell-slash-fill::before { content: "\f630"; } -.bi-bell-slash::before { content: "\f631"; } -.bi-cash-coin::before { content: "\f632"; } -.bi-check-lg::before { content: "\f633"; } -.bi-coin::before { content: "\f634"; } -.bi-currency-bitcoin::before { content: "\f635"; } -.bi-currency-dollar::before { content: "\f636"; } -.bi-currency-euro::before { content: "\f637"; } -.bi-currency-exchange::before { content: "\f638"; } -.bi-currency-pound::before { content: "\f639"; } -.bi-currency-yen::before { content: "\f63a"; } -.bi-dash-lg::before { content: "\f63b"; } -.bi-exclamation-lg::before { content: "\f63c"; } -.bi-file-earmark-pdf-fill::before { content: "\f63d"; } -.bi-file-earmark-pdf::before { content: "\f63e"; } -.bi-file-pdf-fill::before { content: "\f63f"; } -.bi-file-pdf::before { content: "\f640"; } -.bi-gender-ambiguous::before { content: "\f641"; } -.bi-gender-female::before { content: "\f642"; } -.bi-gender-male::before { content: "\f643"; } -.bi-gender-trans::before { content: "\f644"; } -.bi-headset-vr::before { content: "\f645"; } -.bi-info-lg::before { content: "\f646"; } -.bi-mastodon::before { content: "\f647"; } -.bi-messenger::before { content: "\f648"; } -.bi-piggy-bank-fill::before { content: "\f649"; } -.bi-piggy-bank::before { content: "\f64a"; } -.bi-pin-map-fill::before { content: "\f64b"; } -.bi-pin-map::before { content: "\f64c"; } -.bi-plus-lg::before { content: "\f64d"; } -.bi-question-lg::before { content: "\f64e"; } -.bi-recycle::before { content: "\f64f"; } -.bi-reddit::before { content: "\f650"; } -.bi-safe-fill::before { content: "\f651"; } -.bi-safe2-fill::before { content: "\f652"; } -.bi-safe2::before { content: "\f653"; } -.bi-sd-card-fill::before { content: "\f654"; } -.bi-sd-card::before { content: "\f655"; } -.bi-skype::before { content: "\f656"; } -.bi-slash-lg::before { content: "\f657"; } -.bi-translate::before { content: "\f658"; } -.bi-x-lg::before { content: "\f659"; } -.bi-safe::before { content: "\f65a"; } -.bi-apple::before { content: "\f65b"; } -.bi-microsoft::before { content: "\f65d"; } -.bi-windows::before { content: "\f65e"; } -.bi-behance::before { content: "\f65c"; } -.bi-dribbble::before { content: "\f65f"; } -.bi-line::before { content: "\f660"; } -.bi-medium::before { content: "\f661"; } -.bi-paypal::before { content: "\f662"; } -.bi-pinterest::before { content: "\f663"; } -.bi-signal::before { content: "\f664"; } -.bi-snapchat::before { content: "\f665"; } -.bi-spotify::before { content: "\f666"; } -.bi-stack-overflow::before { content: "\f667"; } -.bi-strava::before { content: "\f668"; } -.bi-wordpress::before { content: "\f669"; } -.bi-vimeo::before { content: "\f66a"; } -.bi-activity::before { content: "\f66b"; } -.bi-easel2-fill::before { content: "\f66c"; } -.bi-easel2::before { content: "\f66d"; } -.bi-easel3-fill::before { content: "\f66e"; } -.bi-easel3::before { content: "\f66f"; } -.bi-fan::before { content: "\f670"; } -.bi-fingerprint::before { content: "\f671"; } -.bi-graph-down-arrow::before { content: "\f672"; } -.bi-graph-up-arrow::before { content: "\f673"; } -.bi-hypnotize::before { content: "\f674"; } -.bi-magic::before { content: "\f675"; } -.bi-person-rolodex::before { content: "\f676"; } -.bi-person-video::before { content: "\f677"; } -.bi-person-video2::before { content: "\f678"; } -.bi-person-video3::before { content: "\f679"; } -.bi-person-workspace::before { content: "\f67a"; } -.bi-radioactive::before { content: "\f67b"; } -.bi-webcam-fill::before { content: "\f67c"; } -.bi-webcam::before { content: "\f67d"; } -.bi-yin-yang::before { content: "\f67e"; } -.bi-bandaid-fill::before { content: "\f680"; } -.bi-bandaid::before { content: "\f681"; } -.bi-bluetooth::before { content: "\f682"; } -.bi-body-text::before { content: "\f683"; } -.bi-boombox::before { content: "\f684"; } -.bi-boxes::before { content: "\f685"; } -.bi-dpad-fill::before { content: "\f686"; } -.bi-dpad::before { content: "\f687"; } -.bi-ear-fill::before { content: "\f688"; } -.bi-ear::before { content: "\f689"; } -.bi-envelope-check-fill::before { content: "\f68b"; } -.bi-envelope-check::before { content: "\f68c"; } -.bi-envelope-dash-fill::before { content: "\f68e"; } -.bi-envelope-dash::before { content: "\f68f"; } -.bi-envelope-exclamation-fill::before { content: "\f691"; } -.bi-envelope-exclamation::before { content: "\f692"; } -.bi-envelope-plus-fill::before { content: "\f693"; } -.bi-envelope-plus::before { content: "\f694"; } -.bi-envelope-slash-fill::before { content: "\f696"; } -.bi-envelope-slash::before { content: "\f697"; } -.bi-envelope-x-fill::before { content: "\f699"; } -.bi-envelope-x::before { content: "\f69a"; } -.bi-explicit-fill::before { content: "\f69b"; } -.bi-explicit::before { content: "\f69c"; } -.bi-git::before { content: "\f69d"; } -.bi-infinity::before { content: "\f69e"; } -.bi-list-columns-reverse::before { content: "\f69f"; } -.bi-list-columns::before { content: "\f6a0"; } -.bi-meta::before { content: "\f6a1"; } -.bi-nintendo-switch::before { content: "\f6a4"; } -.bi-pc-display-horizontal::before { content: "\f6a5"; } -.bi-pc-display::before { content: "\f6a6"; } -.bi-pc-horizontal::before { content: "\f6a7"; } -.bi-pc::before { content: "\f6a8"; } -.bi-playstation::before { content: "\f6a9"; } -.bi-plus-slash-minus::before { content: "\f6aa"; } -.bi-projector-fill::before { content: "\f6ab"; } -.bi-projector::before { content: "\f6ac"; } -.bi-qr-code-scan::before { content: "\f6ad"; } -.bi-qr-code::before { content: "\f6ae"; } -.bi-quora::before { content: "\f6af"; } -.bi-quote::before { content: "\f6b0"; } -.bi-robot::before { content: "\f6b1"; } -.bi-send-check-fill::before { content: "\f6b2"; } -.bi-send-check::before { content: "\f6b3"; } -.bi-send-dash-fill::before { content: "\f6b4"; } -.bi-send-dash::before { content: "\f6b5"; } -.bi-send-exclamation-fill::before { content: "\f6b7"; } -.bi-send-exclamation::before { content: "\f6b8"; } -.bi-send-fill::before { content: "\f6b9"; } -.bi-send-plus-fill::before { content: "\f6ba"; } -.bi-send-plus::before { content: "\f6bb"; } -.bi-send-slash-fill::before { content: "\f6bc"; } -.bi-send-slash::before { content: "\f6bd"; } -.bi-send-x-fill::before { content: "\f6be"; } -.bi-send-x::before { content: "\f6bf"; } -.bi-send::before { content: "\f6c0"; } -.bi-steam::before { content: "\f6c1"; } -.bi-terminal-dash::before { content: "\f6c3"; } -.bi-terminal-plus::before { content: "\f6c4"; } -.bi-terminal-split::before { content: "\f6c5"; } -.bi-ticket-detailed-fill::before { content: "\f6c6"; } -.bi-ticket-detailed::before { content: "\f6c7"; } -.bi-ticket-fill::before { content: "\f6c8"; } -.bi-ticket-perforated-fill::before { content: "\f6c9"; } -.bi-ticket-perforated::before { content: "\f6ca"; } -.bi-ticket::before { content: "\f6cb"; } -.bi-tiktok::before { content: "\f6cc"; } -.bi-window-dash::before { content: "\f6cd"; } -.bi-window-desktop::before { content: "\f6ce"; } -.bi-window-fullscreen::before { content: "\f6cf"; } -.bi-window-plus::before { content: "\f6d0"; } -.bi-window-split::before { content: "\f6d1"; } -.bi-window-stack::before { content: "\f6d2"; } -.bi-window-x::before { content: "\f6d3"; } -.bi-xbox::before { content: "\f6d4"; } -.bi-ethernet::before { content: "\f6d5"; } -.bi-hdmi-fill::before { content: "\f6d6"; } -.bi-hdmi::before { content: "\f6d7"; } -.bi-usb-c-fill::before { content: "\f6d8"; } -.bi-usb-c::before { content: "\f6d9"; } -.bi-usb-fill::before { content: "\f6da"; } -.bi-usb-plug-fill::before { content: "\f6db"; } -.bi-usb-plug::before { content: "\f6dc"; } -.bi-usb-symbol::before { content: "\f6dd"; } -.bi-usb::before { content: "\f6de"; } -.bi-boombox-fill::before { content: "\f6df"; } -.bi-displayport::before { content: "\f6e1"; } -.bi-gpu-card::before { content: "\f6e2"; } -.bi-memory::before { content: "\f6e3"; } -.bi-modem-fill::before { content: "\f6e4"; } -.bi-modem::before { content: "\f6e5"; } -.bi-motherboard-fill::before { content: "\f6e6"; } -.bi-motherboard::before { content: "\f6e7"; } -.bi-optical-audio-fill::before { content: "\f6e8"; } -.bi-optical-audio::before { content: "\f6e9"; } -.bi-pci-card::before { content: "\f6ea"; } -.bi-router-fill::before { content: "\f6eb"; } -.bi-router::before { content: "\f6ec"; } -.bi-thunderbolt-fill::before { content: "\f6ef"; } -.bi-thunderbolt::before { content: "\f6f0"; } -.bi-usb-drive-fill::before { content: "\f6f1"; } -.bi-usb-drive::before { content: "\f6f2"; } -.bi-usb-micro-fill::before { content: "\f6f3"; } -.bi-usb-micro::before { content: "\f6f4"; } -.bi-usb-mini-fill::before { content: "\f6f5"; } -.bi-usb-mini::before { content: "\f6f6"; } -.bi-cloud-haze2::before { content: "\f6f7"; } -.bi-device-hdd-fill::before { content: "\f6f8"; } -.bi-device-hdd::before { content: "\f6f9"; } -.bi-device-ssd-fill::before { content: "\f6fa"; } -.bi-device-ssd::before { content: "\f6fb"; } -.bi-displayport-fill::before { content: "\f6fc"; } -.bi-mortarboard-fill::before { content: "\f6fd"; } -.bi-mortarboard::before { content: "\f6fe"; } -.bi-terminal-x::before { content: "\f6ff"; } -.bi-arrow-through-heart-fill::before { content: "\f700"; } -.bi-arrow-through-heart::before { content: "\f701"; } -.bi-badge-sd-fill::before { content: "\f702"; } -.bi-badge-sd::before { content: "\f703"; } -.bi-bag-heart-fill::before { content: "\f704"; } -.bi-bag-heart::before { content: "\f705"; } -.bi-balloon-fill::before { content: "\f706"; } -.bi-balloon-heart-fill::before { content: "\f707"; } -.bi-balloon-heart::before { content: "\f708"; } -.bi-balloon::before { content: "\f709"; } -.bi-box2-fill::before { content: "\f70a"; } -.bi-box2-heart-fill::before { content: "\f70b"; } -.bi-box2-heart::before { content: "\f70c"; } -.bi-box2::before { content: "\f70d"; } -.bi-braces-asterisk::before { content: "\f70e"; } -.bi-calendar-heart-fill::before { content: "\f70f"; } -.bi-calendar-heart::before { content: "\f710"; } -.bi-calendar2-heart-fill::before { content: "\f711"; } -.bi-calendar2-heart::before { content: "\f712"; } -.bi-chat-heart-fill::before { content: "\f713"; } -.bi-chat-heart::before { content: "\f714"; } -.bi-chat-left-heart-fill::before { content: "\f715"; } -.bi-chat-left-heart::before { content: "\f716"; } -.bi-chat-right-heart-fill::before { content: "\f717"; } -.bi-chat-right-heart::before { content: "\f718"; } -.bi-chat-square-heart-fill::before { content: "\f719"; } -.bi-chat-square-heart::before { content: "\f71a"; } -.bi-clipboard-check-fill::before { content: "\f71b"; } -.bi-clipboard-data-fill::before { content: "\f71c"; } -.bi-clipboard-fill::before { content: "\f71d"; } -.bi-clipboard-heart-fill::before { content: "\f71e"; } -.bi-clipboard-heart::before { content: "\f71f"; } -.bi-clipboard-minus-fill::before { content: "\f720"; } -.bi-clipboard-plus-fill::before { content: "\f721"; } -.bi-clipboard-pulse::before { content: "\f722"; } -.bi-clipboard-x-fill::before { content: "\f723"; } -.bi-clipboard2-check-fill::before { content: "\f724"; } -.bi-clipboard2-check::before { content: "\f725"; } -.bi-clipboard2-data-fill::before { content: "\f726"; } -.bi-clipboard2-data::before { content: "\f727"; } -.bi-clipboard2-fill::before { content: "\f728"; } -.bi-clipboard2-heart-fill::before { content: "\f729"; } -.bi-clipboard2-heart::before { content: "\f72a"; } -.bi-clipboard2-minus-fill::before { content: "\f72b"; } -.bi-clipboard2-minus::before { content: "\f72c"; } -.bi-clipboard2-plus-fill::before { content: "\f72d"; } -.bi-clipboard2-plus::before { content: "\f72e"; } -.bi-clipboard2-pulse-fill::before { content: "\f72f"; } -.bi-clipboard2-pulse::before { content: "\f730"; } -.bi-clipboard2-x-fill::before { content: "\f731"; } -.bi-clipboard2-x::before { content: "\f732"; } -.bi-clipboard2::before { content: "\f733"; } -.bi-emoji-kiss-fill::before { content: "\f734"; } -.bi-emoji-kiss::before { content: "\f735"; } -.bi-envelope-heart-fill::before { content: "\f736"; } -.bi-envelope-heart::before { content: "\f737"; } -.bi-envelope-open-heart-fill::before { content: "\f738"; } -.bi-envelope-open-heart::before { content: "\f739"; } -.bi-envelope-paper-fill::before { content: "\f73a"; } -.bi-envelope-paper-heart-fill::before { content: "\f73b"; } -.bi-envelope-paper-heart::before { content: "\f73c"; } -.bi-envelope-paper::before { content: "\f73d"; } -.bi-filetype-aac::before { content: "\f73e"; } -.bi-filetype-ai::before { content: "\f73f"; } -.bi-filetype-bmp::before { content: "\f740"; } -.bi-filetype-cs::before { content: "\f741"; } -.bi-filetype-css::before { content: "\f742"; } -.bi-filetype-csv::before { content: "\f743"; } -.bi-filetype-doc::before { content: "\f744"; } -.bi-filetype-docx::before { content: "\f745"; } -.bi-filetype-exe::before { content: "\f746"; } -.bi-filetype-gif::before { content: "\f747"; } -.bi-filetype-heic::before { content: "\f748"; } -.bi-filetype-html::before { content: "\f749"; } -.bi-filetype-java::before { content: "\f74a"; } -.bi-filetype-jpg::before { content: "\f74b"; } -.bi-filetype-js::before { content: "\f74c"; } -.bi-filetype-jsx::before { content: "\f74d"; } -.bi-filetype-key::before { content: "\f74e"; } -.bi-filetype-m4p::before { content: "\f74f"; } -.bi-filetype-md::before { content: "\f750"; } -.bi-filetype-mdx::before { content: "\f751"; } -.bi-filetype-mov::before { content: "\f752"; } -.bi-filetype-mp3::before { content: "\f753"; } -.bi-filetype-mp4::before { content: "\f754"; } -.bi-filetype-otf::before { content: "\f755"; } -.bi-filetype-pdf::before { content: "\f756"; } -.bi-filetype-php::before { content: "\f757"; } -.bi-filetype-png::before { content: "\f758"; } -.bi-filetype-ppt::before { content: "\f75a"; } -.bi-filetype-psd::before { content: "\f75b"; } -.bi-filetype-py::before { content: "\f75c"; } -.bi-filetype-raw::before { content: "\f75d"; } -.bi-filetype-rb::before { content: "\f75e"; } -.bi-filetype-sass::before { content: "\f75f"; } -.bi-filetype-scss::before { content: "\f760"; } -.bi-filetype-sh::before { content: "\f761"; } -.bi-filetype-svg::before { content: "\f762"; } -.bi-filetype-tiff::before { content: "\f763"; } -.bi-filetype-tsx::before { content: "\f764"; } -.bi-filetype-ttf::before { content: "\f765"; } -.bi-filetype-txt::before { content: "\f766"; } -.bi-filetype-wav::before { content: "\f767"; } -.bi-filetype-woff::before { content: "\f768"; } -.bi-filetype-xls::before { content: "\f76a"; } -.bi-filetype-xml::before { content: "\f76b"; } -.bi-filetype-yml::before { content: "\f76c"; } -.bi-heart-arrow::before { content: "\f76d"; } -.bi-heart-pulse-fill::before { content: "\f76e"; } -.bi-heart-pulse::before { content: "\f76f"; } -.bi-heartbreak-fill::before { content: "\f770"; } -.bi-heartbreak::before { content: "\f771"; } -.bi-hearts::before { content: "\f772"; } -.bi-hospital-fill::before { content: "\f773"; } -.bi-hospital::before { content: "\f774"; } -.bi-house-heart-fill::before { content: "\f775"; } -.bi-house-heart::before { content: "\f776"; } -.bi-incognito::before { content: "\f777"; } -.bi-magnet-fill::before { content: "\f778"; } -.bi-magnet::before { content: "\f779"; } -.bi-person-heart::before { content: "\f77a"; } -.bi-person-hearts::before { content: "\f77b"; } -.bi-phone-flip::before { content: "\f77c"; } -.bi-plugin::before { content: "\f77d"; } -.bi-postage-fill::before { content: "\f77e"; } -.bi-postage-heart-fill::before { content: "\f77f"; } -.bi-postage-heart::before { content: "\f780"; } -.bi-postage::before { content: "\f781"; } -.bi-postcard-fill::before { content: "\f782"; } -.bi-postcard-heart-fill::before { content: "\f783"; } -.bi-postcard-heart::before { content: "\f784"; } -.bi-postcard::before { content: "\f785"; } -.bi-search-heart-fill::before { content: "\f786"; } -.bi-search-heart::before { content: "\f787"; } -.bi-sliders2-vertical::before { content: "\f788"; } -.bi-sliders2::before { content: "\f789"; } -.bi-trash3-fill::before { content: "\f78a"; } -.bi-trash3::before { content: "\f78b"; } -.bi-valentine::before { content: "\f78c"; } -.bi-valentine2::before { content: "\f78d"; } -.bi-wrench-adjustable-circle-fill::before { content: "\f78e"; } -.bi-wrench-adjustable-circle::before { content: "\f78f"; } -.bi-wrench-adjustable::before { content: "\f790"; } -.bi-filetype-json::before { content: "\f791"; } -.bi-filetype-pptx::before { content: "\f792"; } -.bi-filetype-xlsx::before { content: "\f793"; } -.bi-1-circle-fill::before { content: "\f796"; } -.bi-1-circle::before { content: "\f797"; } -.bi-1-square-fill::before { content: "\f798"; } -.bi-1-square::before { content: "\f799"; } -.bi-2-circle-fill::before { content: "\f79c"; } -.bi-2-circle::before { content: "\f79d"; } -.bi-2-square-fill::before { content: "\f79e"; } -.bi-2-square::before { content: "\f79f"; } -.bi-3-circle-fill::before { content: "\f7a2"; } -.bi-3-circle::before { content: "\f7a3"; } -.bi-3-square-fill::before { content: "\f7a4"; } -.bi-3-square::before { content: "\f7a5"; } -.bi-4-circle-fill::before { content: "\f7a8"; } -.bi-4-circle::before { content: "\f7a9"; } -.bi-4-square-fill::before { content: "\f7aa"; } -.bi-4-square::before { content: "\f7ab"; } -.bi-5-circle-fill::before { content: "\f7ae"; } -.bi-5-circle::before { content: "\f7af"; } -.bi-5-square-fill::before { content: "\f7b0"; } -.bi-5-square::before { content: "\f7b1"; } -.bi-6-circle-fill::before { content: "\f7b4"; } -.bi-6-circle::before { content: "\f7b5"; } -.bi-6-square-fill::before { content: "\f7b6"; } -.bi-6-square::before { content: "\f7b7"; } -.bi-7-circle-fill::before { content: "\f7ba"; } -.bi-7-circle::before { content: "\f7bb"; } -.bi-7-square-fill::before { content: "\f7bc"; } -.bi-7-square::before { content: "\f7bd"; } -.bi-8-circle-fill::before { content: "\f7c0"; } -.bi-8-circle::before { content: "\f7c1"; } -.bi-8-square-fill::before { content: "\f7c2"; } -.bi-8-square::before { content: "\f7c3"; } -.bi-9-circle-fill::before { content: "\f7c6"; } -.bi-9-circle::before { content: "\f7c7"; } -.bi-9-square-fill::before { content: "\f7c8"; } -.bi-9-square::before { content: "\f7c9"; } -.bi-airplane-engines-fill::before { content: "\f7ca"; } -.bi-airplane-engines::before { content: "\f7cb"; } -.bi-airplane-fill::before { content: "\f7cc"; } -.bi-airplane::before { content: "\f7cd"; } -.bi-alexa::before { content: "\f7ce"; } -.bi-alipay::before { content: "\f7cf"; } -.bi-android::before { content: "\f7d0"; } -.bi-android2::before { content: "\f7d1"; } -.bi-box-fill::before { content: "\f7d2"; } -.bi-box-seam-fill::before { content: "\f7d3"; } -.bi-browser-chrome::before { content: "\f7d4"; } -.bi-browser-edge::before { content: "\f7d5"; } -.bi-browser-firefox::before { content: "\f7d6"; } -.bi-browser-safari::before { content: "\f7d7"; } -.bi-c-circle-fill::before { content: "\f7da"; } -.bi-c-circle::before { content: "\f7db"; } -.bi-c-square-fill::before { content: "\f7dc"; } -.bi-c-square::before { content: "\f7dd"; } -.bi-capsule-pill::before { content: "\f7de"; } -.bi-capsule::before { content: "\f7df"; } -.bi-car-front-fill::before { content: "\f7e0"; } -.bi-car-front::before { content: "\f7e1"; } -.bi-cassette-fill::before { content: "\f7e2"; } -.bi-cassette::before { content: "\f7e3"; } -.bi-cc-circle-fill::before { content: "\f7e6"; } -.bi-cc-circle::before { content: "\f7e7"; } -.bi-cc-square-fill::before { content: "\f7e8"; } -.bi-cc-square::before { content: "\f7e9"; } -.bi-cup-hot-fill::before { content: "\f7ea"; } -.bi-cup-hot::before { content: "\f7eb"; } -.bi-currency-rupee::before { content: "\f7ec"; } -.bi-dropbox::before { content: "\f7ed"; } -.bi-escape::before { content: "\f7ee"; } -.bi-fast-forward-btn-fill::before { content: "\f7ef"; } -.bi-fast-forward-btn::before { content: "\f7f0"; } -.bi-fast-forward-circle-fill::before { content: "\f7f1"; } -.bi-fast-forward-circle::before { content: "\f7f2"; } -.bi-fast-forward-fill::before { content: "\f7f3"; } -.bi-fast-forward::before { content: "\f7f4"; } -.bi-filetype-sql::before { content: "\f7f5"; } -.bi-fire::before { content: "\f7f6"; } -.bi-google-play::before { content: "\f7f7"; } -.bi-h-circle-fill::before { content: "\f7fa"; } -.bi-h-circle::before { content: "\f7fb"; } -.bi-h-square-fill::before { content: "\f7fc"; } -.bi-h-square::before { content: "\f7fd"; } -.bi-indent::before { content: "\f7fe"; } -.bi-lungs-fill::before { content: "\f7ff"; } -.bi-lungs::before { content: "\f800"; } -.bi-microsoft-teams::before { content: "\f801"; } -.bi-p-circle-fill::before { content: "\f804"; } -.bi-p-circle::before { content: "\f805"; } -.bi-p-square-fill::before { content: "\f806"; } -.bi-p-square::before { content: "\f807"; } -.bi-pass-fill::before { content: "\f808"; } -.bi-pass::before { content: "\f809"; } -.bi-prescription::before { content: "\f80a"; } -.bi-prescription2::before { content: "\f80b"; } -.bi-r-circle-fill::before { content: "\f80e"; } -.bi-r-circle::before { content: "\f80f"; } -.bi-r-square-fill::before { content: "\f810"; } -.bi-r-square::before { content: "\f811"; } -.bi-repeat-1::before { content: "\f812"; } -.bi-repeat::before { content: "\f813"; } -.bi-rewind-btn-fill::before { content: "\f814"; } -.bi-rewind-btn::before { content: "\f815"; } -.bi-rewind-circle-fill::before { content: "\f816"; } -.bi-rewind-circle::before { content: "\f817"; } -.bi-rewind-fill::before { content: "\f818"; } -.bi-rewind::before { content: "\f819"; } -.bi-train-freight-front-fill::before { content: "\f81a"; } -.bi-train-freight-front::before { content: "\f81b"; } -.bi-train-front-fill::before { content: "\f81c"; } -.bi-train-front::before { content: "\f81d"; } -.bi-train-lightrail-front-fill::before { content: "\f81e"; } -.bi-train-lightrail-front::before { content: "\f81f"; } -.bi-truck-front-fill::before { content: "\f820"; } -.bi-truck-front::before { content: "\f821"; } -.bi-ubuntu::before { content: "\f822"; } -.bi-unindent::before { content: "\f823"; } -.bi-unity::before { content: "\f824"; } -.bi-universal-access-circle::before { content: "\f825"; } -.bi-universal-access::before { content: "\f826"; } -.bi-virus::before { content: "\f827"; } -.bi-virus2::before { content: "\f828"; } -.bi-wechat::before { content: "\f829"; } -.bi-yelp::before { content: "\f82a"; } -.bi-sign-stop-fill::before { content: "\f82b"; } -.bi-sign-stop-lights-fill::before { content: "\f82c"; } -.bi-sign-stop-lights::before { content: "\f82d"; } -.bi-sign-stop::before { content: "\f82e"; } -.bi-sign-turn-left-fill::before { content: "\f82f"; } -.bi-sign-turn-left::before { content: "\f830"; } -.bi-sign-turn-right-fill::before { content: "\f831"; } -.bi-sign-turn-right::before { content: "\f832"; } -.bi-sign-turn-slight-left-fill::before { content: "\f833"; } -.bi-sign-turn-slight-left::before { content: "\f834"; } -.bi-sign-turn-slight-right-fill::before { content: "\f835"; } -.bi-sign-turn-slight-right::before { content: "\f836"; } -.bi-sign-yield-fill::before { content: "\f837"; } -.bi-sign-yield::before { content: "\f838"; } -.bi-ev-station-fill::before { content: "\f839"; } -.bi-ev-station::before { content: "\f83a"; } -.bi-fuel-pump-diesel-fill::before { content: "\f83b"; } -.bi-fuel-pump-diesel::before { content: "\f83c"; } -.bi-fuel-pump-fill::before { content: "\f83d"; } -.bi-fuel-pump::before { content: "\f83e"; } -.bi-0-circle-fill::before { content: "\f83f"; } -.bi-0-circle::before { content: "\f840"; } -.bi-0-square-fill::before { content: "\f841"; } -.bi-0-square::before { content: "\f842"; } -.bi-rocket-fill::before { content: "\f843"; } -.bi-rocket-takeoff-fill::before { content: "\f844"; } -.bi-rocket-takeoff::before { content: "\f845"; } -.bi-rocket::before { content: "\f846"; } -.bi-stripe::before { content: "\f847"; } -.bi-subscript::before { content: "\f848"; } -.bi-superscript::before { content: "\f849"; } -.bi-trello::before { content: "\f84a"; } -.bi-envelope-at-fill::before { content: "\f84b"; } -.bi-envelope-at::before { content: "\f84c"; } -.bi-regex::before { content: "\f84d"; } -.bi-text-wrap::before { content: "\f84e"; } -.bi-sign-dead-end-fill::before { content: "\f84f"; } -.bi-sign-dead-end::before { content: "\f850"; } -.bi-sign-do-not-enter-fill::before { content: "\f851"; } -.bi-sign-do-not-enter::before { content: "\f852"; } -.bi-sign-intersection-fill::before { content: "\f853"; } -.bi-sign-intersection-side-fill::before { content: "\f854"; } -.bi-sign-intersection-side::before { content: "\f855"; } -.bi-sign-intersection-t-fill::before { content: "\f856"; } -.bi-sign-intersection-t::before { content: "\f857"; } -.bi-sign-intersection-y-fill::before { content: "\f858"; } -.bi-sign-intersection-y::before { content: "\f859"; } -.bi-sign-intersection::before { content: "\f85a"; } -.bi-sign-merge-left-fill::before { content: "\f85b"; } -.bi-sign-merge-left::before { content: "\f85c"; } -.bi-sign-merge-right-fill::before { content: "\f85d"; } -.bi-sign-merge-right::before { content: "\f85e"; } -.bi-sign-no-left-turn-fill::before { content: "\f85f"; } -.bi-sign-no-left-turn::before { content: "\f860"; } -.bi-sign-no-parking-fill::before { content: "\f861"; } -.bi-sign-no-parking::before { content: "\f862"; } -.bi-sign-no-right-turn-fill::before { content: "\f863"; } -.bi-sign-no-right-turn::before { content: "\f864"; } -.bi-sign-railroad-fill::before { content: "\f865"; } -.bi-sign-railroad::before { content: "\f866"; } -.bi-building-add::before { content: "\f867"; } -.bi-building-check::before { content: "\f868"; } -.bi-building-dash::before { content: "\f869"; } -.bi-building-down::before { content: "\f86a"; } -.bi-building-exclamation::before { content: "\f86b"; } -.bi-building-fill-add::before { content: "\f86c"; } -.bi-building-fill-check::before { content: "\f86d"; } -.bi-building-fill-dash::before { content: "\f86e"; } -.bi-building-fill-down::before { content: "\f86f"; } -.bi-building-fill-exclamation::before { content: "\f870"; } -.bi-building-fill-gear::before { content: "\f871"; } -.bi-building-fill-lock::before { content: "\f872"; } -.bi-building-fill-slash::before { content: "\f873"; } -.bi-building-fill-up::before { content: "\f874"; } -.bi-building-fill-x::before { content: "\f875"; } -.bi-building-fill::before { content: "\f876"; } -.bi-building-gear::before { content: "\f877"; } -.bi-building-lock::before { content: "\f878"; } -.bi-building-slash::before { content: "\f879"; } -.bi-building-up::before { content: "\f87a"; } -.bi-building-x::before { content: "\f87b"; } -.bi-buildings-fill::before { content: "\f87c"; } -.bi-buildings::before { content: "\f87d"; } -.bi-bus-front-fill::before { content: "\f87e"; } -.bi-bus-front::before { content: "\f87f"; } -.bi-ev-front-fill::before { content: "\f880"; } -.bi-ev-front::before { content: "\f881"; } -.bi-globe-americas::before { content: "\f882"; } -.bi-globe-asia-australia::before { content: "\f883"; } -.bi-globe-central-south-asia::before { content: "\f884"; } -.bi-globe-europe-africa::before { content: "\f885"; } -.bi-house-add-fill::before { content: "\f886"; } -.bi-house-add::before { content: "\f887"; } -.bi-house-check-fill::before { content: "\f888"; } -.bi-house-check::before { content: "\f889"; } -.bi-house-dash-fill::before { content: "\f88a"; } -.bi-house-dash::before { content: "\f88b"; } -.bi-house-down-fill::before { content: "\f88c"; } -.bi-house-down::before { content: "\f88d"; } -.bi-house-exclamation-fill::before { content: "\f88e"; } -.bi-house-exclamation::before { content: "\f88f"; } -.bi-house-gear-fill::before { content: "\f890"; } -.bi-house-gear::before { content: "\f891"; } -.bi-house-lock-fill::before { content: "\f892"; } -.bi-house-lock::before { content: "\f893"; } -.bi-house-slash-fill::before { content: "\f894"; } -.bi-house-slash::before { content: "\f895"; } -.bi-house-up-fill::before { content: "\f896"; } -.bi-house-up::before { content: "\f897"; } -.bi-house-x-fill::before { content: "\f898"; } -.bi-house-x::before { content: "\f899"; } -.bi-person-add::before { content: "\f89a"; } -.bi-person-down::before { content: "\f89b"; } -.bi-person-exclamation::before { content: "\f89c"; } -.bi-person-fill-add::before { content: "\f89d"; } -.bi-person-fill-check::before { content: "\f89e"; } -.bi-person-fill-dash::before { content: "\f89f"; } -.bi-person-fill-down::before { content: "\f8a0"; } -.bi-person-fill-exclamation::before { content: "\f8a1"; } -.bi-person-fill-gear::before { content: "\f8a2"; } -.bi-person-fill-lock::before { content: "\f8a3"; } -.bi-person-fill-slash::before { content: "\f8a4"; } -.bi-person-fill-up::before { content: "\f8a5"; } -.bi-person-fill-x::before { content: "\f8a6"; } -.bi-person-gear::before { content: "\f8a7"; } -.bi-person-lock::before { content: "\f8a8"; } -.bi-person-slash::before { content: "\f8a9"; } -.bi-person-up::before { content: "\f8aa"; } -.bi-scooter::before { content: "\f8ab"; } -.bi-taxi-front-fill::before { content: "\f8ac"; } -.bi-taxi-front::before { content: "\f8ad"; } -.bi-amd::before { content: "\f8ae"; } -.bi-database-add::before { content: "\f8af"; } -.bi-database-check::before { content: "\f8b0"; } -.bi-database-dash::before { content: "\f8b1"; } -.bi-database-down::before { content: "\f8b2"; } -.bi-database-exclamation::before { content: "\f8b3"; } -.bi-database-fill-add::before { content: "\f8b4"; } -.bi-database-fill-check::before { content: "\f8b5"; } -.bi-database-fill-dash::before { content: "\f8b6"; } -.bi-database-fill-down::before { content: "\f8b7"; } -.bi-database-fill-exclamation::before { content: "\f8b8"; } -.bi-database-fill-gear::before { content: "\f8b9"; } -.bi-database-fill-lock::before { content: "\f8ba"; } -.bi-database-fill-slash::before { content: "\f8bb"; } -.bi-database-fill-up::before { content: "\f8bc"; } -.bi-database-fill-x::before { content: "\f8bd"; } -.bi-database-fill::before { content: "\f8be"; } -.bi-database-gear::before { content: "\f8bf"; } -.bi-database-lock::before { content: "\f8c0"; } -.bi-database-slash::before { content: "\f8c1"; } -.bi-database-up::before { content: "\f8c2"; } -.bi-database-x::before { content: "\f8c3"; } -.bi-database::before { content: "\f8c4"; } -.bi-houses-fill::before { content: "\f8c5"; } -.bi-houses::before { content: "\f8c6"; } -.bi-nvidia::before { content: "\f8c7"; } -.bi-person-vcard-fill::before { content: "\f8c8"; } -.bi-person-vcard::before { content: "\f8c9"; } -.bi-sina-weibo::before { content: "\f8ca"; } -.bi-tencent-qq::before { content: "\f8cb"; } -.bi-wikipedia::before { content: "\f8cc"; } -.bi-alphabet-uppercase::before { content: "\f2a5"; } -.bi-alphabet::before { content: "\f68a"; } -.bi-amazon::before { content: "\f68d"; } -.bi-arrows-collapse-vertical::before { content: "\f690"; } -.bi-arrows-expand-vertical::before { content: "\f695"; } -.bi-arrows-vertical::before { content: "\f698"; } -.bi-arrows::before { content: "\f6a2"; } -.bi-ban-fill::before { content: "\f6a3"; } -.bi-ban::before { content: "\f6b6"; } -.bi-bing::before { content: "\f6c2"; } -.bi-cake::before { content: "\f6e0"; } -.bi-cake2::before { content: "\f6ed"; } -.bi-cookie::before { content: "\f6ee"; } -.bi-copy::before { content: "\f759"; } -.bi-crosshair::before { content: "\f769"; } -.bi-crosshair2::before { content: "\f794"; } -.bi-emoji-astonished-fill::before { content: "\f795"; } -.bi-emoji-astonished::before { content: "\f79a"; } -.bi-emoji-grimace-fill::before { content: "\f79b"; } -.bi-emoji-grimace::before { content: "\f7a0"; } -.bi-emoji-grin-fill::before { content: "\f7a1"; } -.bi-emoji-grin::before { content: "\f7a6"; } -.bi-emoji-surprise-fill::before { content: "\f7a7"; } -.bi-emoji-surprise::before { content: "\f7ac"; } -.bi-emoji-tear-fill::before { content: "\f7ad"; } -.bi-emoji-tear::before { content: "\f7b2"; } -.bi-envelope-arrow-down-fill::before { content: "\f7b3"; } -.bi-envelope-arrow-down::before { content: "\f7b8"; } -.bi-envelope-arrow-up-fill::before { content: "\f7b9"; } -.bi-envelope-arrow-up::before { content: "\f7be"; } -.bi-feather::before { content: "\f7bf"; } -.bi-feather2::before { content: "\f7c4"; } -.bi-floppy-fill::before { content: "\f7c5"; } -.bi-floppy::before { content: "\f7d8"; } -.bi-floppy2-fill::before { content: "\f7d9"; } -.bi-floppy2::before { content: "\f7e4"; } -.bi-gitlab::before { content: "\f7e5"; } -.bi-highlighter::before { content: "\f7f8"; } -.bi-marker-tip::before { content: "\f802"; } -.bi-nvme-fill::before { content: "\f803"; } -.bi-nvme::before { content: "\f80c"; } -.bi-opencollective::before { content: "\f80d"; } -.bi-pci-card-network::before { content: "\f8cd"; } -.bi-pci-card-sound::before { content: "\f8ce"; } -.bi-radar::before { content: "\f8cf"; } -.bi-send-arrow-down-fill::before { content: "\f8d0"; } -.bi-send-arrow-down::before { content: "\f8d1"; } -.bi-send-arrow-up-fill::before { content: "\f8d2"; } -.bi-send-arrow-up::before { content: "\f8d3"; } -.bi-sim-slash-fill::before { content: "\f8d4"; } -.bi-sim-slash::before { content: "\f8d5"; } -.bi-sourceforge::before { content: "\f8d6"; } -.bi-substack::before { content: "\f8d7"; } -.bi-threads-fill::before { content: "\f8d8"; } -.bi-threads::before { content: "\f8d9"; } -.bi-transparency::before { content: "\f8da"; } -.bi-twitter-x::before { content: "\f8db"; } -.bi-type-h4::before { content: "\f8dc"; } -.bi-type-h5::before { content: "\f8dd"; } -.bi-type-h6::before { content: "\f8de"; } -.bi-backpack-fill::before { content: "\f8df"; } -.bi-backpack::before { content: "\f8e0"; } -.bi-backpack2-fill::before { content: "\f8e1"; } -.bi-backpack2::before { content: "\f8e2"; } -.bi-backpack3-fill::before { content: "\f8e3"; } -.bi-backpack3::before { content: "\f8e4"; } -.bi-backpack4-fill::before { content: "\f8e5"; } -.bi-backpack4::before { content: "\f8e6"; } -.bi-brilliance::before { content: "\f8e7"; } -.bi-cake-fill::before { content: "\f8e8"; } -.bi-cake2-fill::before { content: "\f8e9"; } -.bi-duffle-fill::before { content: "\f8ea"; } -.bi-duffle::before { content: "\f8eb"; } -.bi-exposure::before { content: "\f8ec"; } -.bi-gender-neuter::before { content: "\f8ed"; } -.bi-highlights::before { content: "\f8ee"; } -.bi-luggage-fill::before { content: "\f8ef"; } -.bi-luggage::before { content: "\f8f0"; } -.bi-mailbox-flag::before { content: "\f8f1"; } -.bi-mailbox2-flag::before { content: "\f8f2"; } -.bi-noise-reduction::before { content: "\f8f3"; } -.bi-passport-fill::before { content: "\f8f4"; } -.bi-passport::before { content: "\f8f5"; } -.bi-person-arms-up::before { content: "\f8f6"; } -.bi-person-raised-hand::before { content: "\f8f7"; } -.bi-person-standing-dress::before { content: "\f8f8"; } -.bi-person-standing::before { content: "\f8f9"; } -.bi-person-walking::before { content: "\f8fa"; } -.bi-person-wheelchair::before { content: "\f8fb"; } -.bi-shadows::before { content: "\f8fc"; } -.bi-suitcase-fill::before { content: "\f8fd"; } -.bi-suitcase-lg-fill::before { content: "\f8fe"; } -.bi-suitcase-lg::before { content: "\f8ff"; } -.bi-suitcase::before { content: "\f900"; } -.bi-suitcase2-fill::before { content: "\f901"; } -.bi-suitcase2::before { content: "\f902"; } -.bi-vignette::before { content: "\f903"; } -.bi-bluesky::before { content: "\f7f9"; } -.bi-tux::before { content: "\f904"; } -.bi-beaker-fill::before { content: "\f905"; } -.bi-beaker::before { content: "\f906"; } -.bi-flask-fill::before { content: "\f907"; } -.bi-flask-florence-fill::before { content: "\f908"; } -.bi-flask-florence::before { content: "\f909"; } -.bi-flask::before { content: "\f90a"; } -.bi-leaf-fill::before { content: "\f90b"; } -.bi-leaf::before { content: "\f90c"; } -.bi-measuring-cup-fill::before { content: "\f90d"; } -.bi-measuring-cup::before { content: "\f90e"; } -.bi-unlock2-fill::before { content: "\f90f"; } -.bi-unlock2::before { content: "\f910"; } -.bi-battery-low::before { content: "\f911"; } -.bi-anthropic::before { content: "\f912"; } -.bi-apple-music::before { content: "\f913"; } -.bi-claude::before { content: "\f914"; } -.bi-openai::before { content: "\f915"; } -.bi-perplexity::before { content: "\f916"; } -.bi-css::before { content: "\f917"; } -.bi-javascript::before { content: "\f918"; } -.bi-typescript::before { content: "\f919"; } -.bi-fork-knife::before { content: "\f91a"; } -.bi-globe-americas-fill::before { content: "\f91b"; } -.bi-globe-asia-australia-fill::before { content: "\f91c"; } -.bi-globe-central-south-asia-fill::before { content: "\f91d"; } -.bi-globe-europe-africa-fill::before { content: "\f91e"; } -/*! - * Bootstrap v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;line-height:inherit;font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button{cursor:pointer;filter:grayscale(1)}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-weight:300;line-height:1.2;font-size:calc(1.625rem + 4.5vw)}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-weight:300;line-height:1.2;font-size:calc(1.575rem + 3.9vw)}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-weight:300;line-height:1.2;font-size:calc(1.525rem + 3.3vw)}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-weight:300;line-height:1.2;font-size:calc(1.475rem + 2.7vw)}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-weight:300;line-height:1.2;font-size:calc(1.425rem + 2.1vw)}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-weight:300;line-height:1.2;font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#a6b5cc;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#b5b6b7;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#a7b9b1;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#a6c3ca;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#ccc2a4;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#c6acae;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#c6c7c8;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#4d5154;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;max-width:100%;height:100%;padding:1rem .75rem;overflow:hidden;color:rgba(var(--bs-body-color-rgb),.65);text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem;padding-left:.75rem}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>textarea:focus~label::after,.form-floating>textarea:not(:placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>textarea:disabled~label::after{background-color:var(--bs-secondary-bg)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(-1 * var(--bs-border-width));border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(-1 * var(--bs-border-width))}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(-1 * var(--bs-border-width))}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-grow:1;flex-basis:0;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-grow:1;flex-basis:100%;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child)>.card-header,.card-group>.card:not(:last-child)>.card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child)>.card-footer,.card-group>.card:not(:last-child)>.card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child)>.card-header,.card-group>.card:not(:first-child)>.card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child)>.card-footer,.card-group>.card:not(:first-child)>.card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-collapse,.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(-1 * var(--bs-border-width))}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:var(--bs-progress-height)}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:not(.active):focus,.list-group-item-action:not(.active):hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:not(.active):active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;filter:var(--bs-btn-close-filter);border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}:root,[data-bs-theme=light]{--bs-btn-close-filter: }[data-bs-theme=dark]{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color:var(--bs-body-color);--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0,-50px);transition:transform .3s ease-out}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin-top:calc(-.5 * var(--bs-modal-header-padding-y));margin-right:calc(-.5 * var(--bs-modal-header-padding-x));margin-bottom:calc(-.5 * var(--bs-modal-header-padding-y));margin-left:auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;filter:var(--bs-carousel-control-icon-filter);border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:var(--bs-carousel-indicator-active-bg);background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:var(--bs-carousel-caption-color);text-align:center}.carousel-dark{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}:root,[data-bs-theme=light]{--bs-carousel-indicator-active-bg:#fff;--bs-carousel-caption-color:#fff;--bs-carousel-control-icon-filter: }[data-bs-theme=dark]{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}.spinner-border,.spinner-grow{display:inline-block;flex-shrink:0;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y));margin-left:auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.visually-hidden *,.visually-hidden-focusable:not(:focus):not(:focus-within) *{overflow:hidden!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map *//** - * prism.js default theme for JavaScript, CSS and HTML - * Based on dabblet (http://dabblet.com) - * @author Lea Verou - */ - -code[class*="language-"], -pre[class*="language-"] { - color: black; - background: none; - text-shadow: 0 1px white; - font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; - font-size: 1em; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; - - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} - -pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, -code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { - text-shadow: none; - background: #b3d4fc; -} - -pre[class*="language-"]::selection, pre[class*="language-"] ::selection, -code[class*="language-"]::selection, code[class*="language-"] ::selection { - text-shadow: none; - background: #b3d4fc; -} - -@media print { - code[class*="language-"], - pre[class*="language-"] { - text-shadow: none; - } -} - -/* Code blocks */ -pre[class*="language-"] { - padding: 1em; - margin: .5em 0; - overflow: auto; -} - -:not(pre) > code[class*="language-"], -pre[class*="language-"] { - background: #f5f2f0; -} - -/* Inline code */ -:not(pre) > code[class*="language-"] { - padding: .1em; - border-radius: .3em; - white-space: normal; -} - -.token.comment, -.token.prolog, -.token.doctype, -.token.cdata { - color: slategray; -} - -.token.punctuation { - color: #999; -} - -.token.namespace { - opacity: .7; -} - -.token.property, -.token.tag, -.token.boolean, -.token.number, -.token.constant, -.token.symbol, -.token.deleted { - color: #905; -} - -.token.selector, -.token.attr-name, -.token.string, -.token.char, -.token.builtin, -.token.inserted { - color: #690; -} - -.token.operator, -.token.entity, -.token.url, -.language-css .token.string, -.style .token.string { - color: #9a6e3a; - /* This background color was intended by the author of this theme. */ - background: hsla(0, 0%, 100%, .5); -} - -.token.atrule, -.token.attr-value, -.token.keyword { - color: #07a; -} - -.token.function, -.token.class-name { - color: #DD4A68; -} - -.token.regex, -.token.important, -.token.variable { - color: #e90; -} - -.token.important, -.token.bold { - font-weight: bold; -} -.token.italic { - font-style: italic; -} - -.token.entity { - cursor: help; -} -:root { - --bs-border-radius: 3px; - --bs-border-radius-lg: 4px; - --bs-popover-max-width: 50%; - --inspect-find-background: var(--bs-body-bg); - --inspect-find-foreground: var(--bs-body-color); - --inspect-input-background: var(--bs-body-bg); - --inspect-input-foreground: var(--bs-body-color); - --inspect-input-border: var(--bs-light-border-subtle); - --inspect-diff-add-color: #dafbe1; - --inspect-diff-remove-color: #ffebe9; - --inspect-inactive-selection-background: var( - --vscode-editor-inactiveSelectionBackground, - #d9d9d9 - ); - --inspect-active-selection-background: var( - --vscode-editor-selectionBackground, - #d7d4f0 - ); - --inspect-focus-border-color: #86b7fe; - --inspect-focus-border-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25); - --inspect-focus-border-gray-color: #808080; - --inspect-focus-border-gray-shadow: 0 0 0 0.25rem rgba(48, 48, 48, 0.25); - - /* Inspect Font Sizes */ - --inspect-font-size-title: 1.5rem; - --inspect-font-size-title-secondary: 1.3rem; - --inspect-font-size-largest: 1.2rem; - --inspect-font-size-larger: 1.1rem; - --inspect-font-size-large: 1rem; - --inspect-font-size-base: 0.9rem; - --inspect-font-size-small: 0.8rem; - --inspect-font-size-smaller: 0.8rem; - --inspect-font-size-smallest: 0.7rem; - --inspect-font-size-smallestest: 0.6rem; - - /* Inspect Glass */ - --inspect-glass-color: #000000; - --inspect-glass-opacity: 0.3; - - /* VS Code Light Modern theme defaults (for non-VS Code context) */ - /* Complete set of variables used by @vscode-elements/elements */ - /* These are overridden by actual VS Code theme variables when running in VS Code */ - - /* Core colors */ - --vscode-foreground: #3b3b3b; - --vscode-descriptionForeground: #717171; - --vscode-errorForeground: #f14c4c; - --vscode-icon-foreground: #424242; - --vscode-focusBorder: #0090f1; - --vscode-contrastBorder: transparent; - - /* Font settings */ - --vscode-font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, - sans-serif; - --vscode-font-size: 13px; - --vscode-font-weight: normal; - --vscode-editor-font-family: Menlo, Monaco, "Courier New", monospace; - --vscode-editor-font-size: 12px; - --vscode-editor-font-weight: normal; - - /* Editor colors */ - --vscode-editor-background: #ffffff; - --vscode-editor-foreground: #3b3b3b; - --vscode-editor-inlineValuesForeground: rgba(0, 0, 0, 0.5); - --vscode-editorGroup-border: #e7e7e7; - --vscode-editorWidget-border: #d4d4d4; - - /* Input colors */ - --vscode-input-background: #ffffff; - --vscode-input-foreground: #3b3b3b; - --vscode-input-border: #cecece; - --vscode-input-placeholderForeground: #767676; - --vscode-inputOption-activeBackground: rgba(0, 144, 241, 0.2); - --vscode-inputOption-activeBorder: #0090f1; - --vscode-inputOption-activeForeground: #000000; - - /* Input validation */ - --vscode-inputValidation-warningBackground: #f2dede; - --vscode-inputValidation-warningBorder: #be8800; - --vscode-inputValidation-errorBackground: #f2dede; - --vscode-inputValidation-errorBorder: #be1100; - - /* Settings controls (used by vscode-elements textfield, checkbox, dropdown) */ - --vscode-settings-textInputBackground: #ffffff; - --vscode-settings-textInputForeground: #3b3b3b; - --vscode-settings-textInputBorder: #cecece; - --vscode-settings-checkboxBackground: #ffffff; - --vscode-settings-checkboxForeground: #3b3b3b; - --vscode-settings-checkboxBorder: #919191; - --vscode-settings-dropdownBackground: #ffffff; - --vscode-settings-dropdownForeground: #3b3b3b; - --vscode-settings-dropdownBorder: #cecece; - --vscode-settings-dropdownListBorder: #c8c8c8; - --vscode-settings-headerBorder: rgba(128, 128, 128, 0.35); - - /* Checkbox colors */ - --vscode-checkbox-background: #ffffff; - --vscode-checkbox-foreground: #3b3b3b; - --vscode-checkbox-border: #919191; - - /* Button colors */ - --vscode-button-background: #007acc; - --vscode-button-foreground: #ffffff; - --vscode-button-hoverBackground: #0062a3; - --vscode-button-border: transparent; - --vscode-button-separator: rgba(255, 255, 255, 0.4); - --vscode-button-secondaryBackground: #5f6a79; - --vscode-button-secondaryForeground: #ffffff; - --vscode-button-secondaryHoverBackground: #4c5561; - - /* Widget/shadow colors */ - --vscode-widget-shadow: rgba(0, 0, 0, 0.16); - --vscode-widget-border: #d4d4d4; - - /* Toolbar */ - --vscode-toolbar-hoverBackground: rgba(184, 184, 184, 0.31); - --vscode-toolbar-activeBackground: rgba(166, 166, 166, 0.31); - --vscode-toolbar-hoverOutline: transparent; - - /* List colors (used by select/dropdown components) */ - --vscode-list-hoverBackground: #e8e8e8; - --vscode-list-hoverForeground: #3b3b3b; - --vscode-list-activeSelectionBackground: #0060c0; - --vscode-list-activeSelectionForeground: #ffffff; - --vscode-list-activeSelectionIconForeground: #ffffff; - --vscode-list-inactiveSelectionBackground: #e4e6f1; - --vscode-list-focusOutline: #0090f1; - --vscode-list-focusAndSelectionOutline: #0090f1; - --vscode-list-focusHighlightForeground: #0066bf; - --vscode-list-highlightForeground: #0066bf; - - /* Menu colors */ - --vscode-menu-background: #ffffff; - --vscode-menu-foreground: #3b3b3b; - --vscode-menu-border: #d4d4d4; - --vscode-menu-selectionBackground: #0060c0; - --vscode-menu-selectionForeground: #ffffff; - --vscode-menu-selectionBorder: transparent; - --vscode-menu-separatorBackground: #d4d4d4; - - /* Badge colors */ - --vscode-badge-background: #007acc; - --vscode-badge-foreground: #ffffff; - --vscode-activityBarBadge-background: #007acc; - --vscode-activityBarBadge-foreground: #ffffff; - - /* Panel colors */ - --vscode-panel-background: #ffffff; - --vscode-panelTitle-activeBorder: #007acc; - --vscode-panelTitle-activeForeground: #3b3b3b; - --vscode-panelTitle-inactiveForeground: #717171; - - /* Sidebar colors */ - --vscode-sideBar-background: #f3f3f3; - --vscode-sideBarSectionHeader-background: #f3f3f3; - --vscode-sideBarTitle-foreground: #3b3b3b; - - /* Progress bar */ - --vscode-progressBar-background: #007acc; - - /* Scrollbar colors */ - --vscode-scrollbar-shadow: rgba(0, 0, 0, 0.16); - --vscode-scrollbarSlider-background: rgba(100, 100, 100, 0.4); - --vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7); - --vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6); - - /* Sash (resizable borders) */ - --vscode-sash-hoverBorder: #0090f1; - - /* Tree colors */ - --vscode-tree-indentGuidesStroke: #d4d4d4; - --vscode-tree-inactiveIndentGuidesStroke: rgba(212, 212, 212, 0.5); - - /* Keybinding table */ - --vscode-keybindingTable-headerBackground: rgba(128, 128, 128, 0.1); - --vscode-keybindingTable-rowsBackground: rgba(128, 128, 128, 0.04); -} - -body:not([class^="vscode-"]) button { - --bs-nav-pills-link-active-bg: #e3eaf1; - --bs-nav-pills-link-active-color: black; - --bs-nav-link-color: black; -} - -/* vscode-elements form component adjustments */ -vscode-label { - margin-bottom: 0; -} - -vscode-form-helper { - font-size: 12px; - margin-top: 0; - margin-bottom: 6px; -} - -/* AG Grid Theming */ -body { - --ag-background-color: var(--bs-body-bg); - --ag-foreground-color: var(--bs-body-color); - --ag-header-background-color: var(--bs-light-bg-subtle); - --ag-header-font-size: var(--inspect-font-size-small); - --ag-header-font-weight: 500; - --ag-header-text-color: var(--bs-secondary); - --ag-font-family: var(--bs-body-font-family); - --ag-wrapper-border: none; - --ag-row-hover-color: var(--bs-secondary-bg-subtle); - --ag-selected-row-background-color: var(--bs-secondary-bg-subtle); - --ag-border-radius: var(--bs-border-radius); -} - -.ag-cell-auto-height { - --reduceLineHeightBy: 16px; - line-height: calc( - var(--ag-internal-calculated-line-height) - var(--reduceLineHeightBy) - ); - padding-top: calc(var(--reduceLineHeightBy) / 2); - padding-bottom: calc(var(--reduceLineHeightBy) / 2); -} - -#app { - height: 100vh; - overflow-y: hidden; -} - -.app-main-grid { - display: grid; - height: 100vh; - overflow-y: hidden; - grid-template-rows: max-content max-content 1fr; -} - -a { - color: var(--bs-link-color); -} - -a:hover { - color: var(--bs-link-hover-color); -} - -.modal { - --bs-modal-margin: 0.25rem; -} - -.modal-backdrop { - --bs-backdrop-opacity: 0.4; -} - -.app-main-grid.single-file-mode { - grid-template-rows: max-content max-content 1fr; -} - -/* Inspect Text Styles */ -.text-style-label { - text-transform: uppercase !important; -} - -.text-style-secondary { - color: var(--bs-secondary) !important; -} - -.text-style-tertiary { - color: var(--bs-tertiary-color) !important; -} - -/* Inspect Font Size Styles */ -.text-size-title { - font-size: var(--inspect-font-size-title) !important; -} - -.text-size-title-secondary { - font-size: var(--inspect-font-size-title-secondary) !important; -} - -.text-size-largest { - font-size: var(--inspect-font-size-largest) !important; -} - -.text-size-larger { - font-size: var(--inspect-font-size-larger) !important; -} - -.text-size-large { - font-size: var(--inspect-font-size-large) !important; -} - -.text-size-base { - font-size: var(--inspect-font-size-base) !important; -} - -.text-size-small { - font-size: var(--inspect-font-size-small) !important; -} - -.text-size-smaller { - font-size: var(--inspect-font-size-smaller) !important; -} - -.text-size-smallest { - font-size: var(--inspect-font-size-smallest) !important; -} - -.text-size-smallestest { - font-size: var(--inspect-font-size-smallestest) !important; -} - -.text-truncate { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -} - -.three-line-clamp { - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - max-height: calc(3em * var(--bs-body-line-height)); -} - -body[class^="vscode-"] { - --bs-border-radius: 0; - --bs-border-radius-lg: 0; - --bs-body-bg: var(--vscode-editor-background); - --bs-secondary-bg-subtle: var(--vscode-editorHoverWidget-background); - --bs-primary-text-emphasis: var(--vscode-editorLink-activeForeground); - --bs-card-bg: var(--vscode-editor-background); - --bs-table-bg: var(--vscode-editor-background); - --bs-light-bg-subtle: var(--vscode-sideBar-background); - --bs-light-border-subtle: var(--vscode-sideBarSectionHeader-border); - --bs-body-color: var(--vscode-editor-foreground); - --bs-table-color: var(--vscode-editor-foreground); - --bs-accordion-btn-color: var(--vscode-editor-foreground); - --bs-emphasis-color: var(--vscode-editor-foreground); - --bs-navbar-brand-color: var(--vscode-editor-foreground); - --bs-navbar-brand-hover-color: var(--vscode-editor-foreground); - --bs-code-color: var(--vscode-editorInfo-foreground); - --bs-light: var(--vscode-sideBar-background); - --bs-btn-bg: var(--vscode-peekViewTitle-background); - --bs-primary: var(--vscode-banner-iconForeground); - --bs-primary-bg-subtle: var(--vscode-editor-selectionHighlightBackground); - --bs-nav-pills-link-active-bg: var(--vscode-banner-iconForeground); - --bs-link-color: var(--vscode-textLink-foreground); - --bs-link-hover-color: var(--vscode-textLink-activeForeground); - --bs-secondary: var(--vscode-breadcrumb-foreground); - --bs-secondary-bg: var(--vscode-list-inactiveSelectionBackground); - --bs-border-color: var(--vscode-editorGroup-border); - --bs-card-border-color: var(--vscode-editorGroup-border); - --bs-warning-bg-subtle: var(--vscode-inputValidation-warningBackground); - --bs-warning-text-emphasis: var(--vscode-input-foreground); - --bs-breadcrumb-divider-color: var(--vscode-foreground); - --inspect-find-background: var(--vscode-editorWidget-background); - --inspect-find-foreground: var(--vscode-editorWidget-foreground); - --inspect-input-background: var(--vscode-input-background); - --inspect-input-foreground: var(--vscode-input-foreground); - --inspect-input-border: var(--vscode-input-border); - --inspect-diff-add-color: var(--vscode-diffEditor-insertedTextBackground); - --inspect-diff-remove-color: var(--vscode-diffEditor-removedTextBackground); - --inspect-glass-color: var(--vscode-editor-foreground); - --inspect-glass-opacity: 0.15; -} - -html.vscode { - font-size: 13px; -} - -html.vscode .sample-input { - line-height: 1.3em; - -webkit-line-clamp: 4 !important; -} - -body[class^="vscode-"] .modal-backdrop { - --bs-backdrop-opacity: 0.15; - --bs-backdrop-bg: var(--vscode-editor-foreground); -} - -body[class^="vscode-"] code { - background-color: transparent; -} - -body[class^="vscode-"] .popover.rendered-content, -body[class^="vscode-"] .modal-dialog { - border: solid 1px var(--bs-border-color); -} - -body[class^="vscode-"] .popover-arrow::before { - border-left-color: var(--bs-border-color) !important; -} - -body[class^="vscode-"] - [data-popper-placement^="bottom"] - > .popper-arrow-container - > div, -body[class^="vscode-"] - [data-popper-placement^="left"] - > .popper-arrow-container - > div, -body[class^="vscode-"] - [data-popper-placement^="right"] - > .popper-arrow-container - > div { - border-color: transparent transparent var(--bs-card-border-color) !important; -} - -body[class^="vscode-"] .modal-content { - background-clip: unset; -} - -body[class^="vscode-"] .multi-score-label { - margin-bottom: 5px; -} - -body[class^="vscode-"] { - min-width: 400px; -} - -body[class^="vscode-"] .navbar-brand { - font-size: 1.1em; -} -body[class^="vscode-"] .navbar-brand > div { - margin-top: -0.2rem !important; -} - -body[class^="vscode-"] .task-title { - margin-top: 0.4em; -} - -body[class^="vscode-"] .task-model { - margin-top: 0.2rem; - font-size: 0.9rem; -} - -body[class^="vscode-"] .accordion-button::after { - --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='black'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); - background: white; - mask-image: var(--bs-accordion-btn-icon); -} - -.copy-button i.bi, -body[class^="vscode-"] .navbar-text i.bi { - color: var(--vscode-editor-foreground); -} - -body[class^="vscode-"] .btn-tools { - --bs-btn-hover-bg: var(--vscode-peekViewTitle-background); - --bs-btn-bg: var(--vscode-peekViewTitle-background); - --bs-btn-border-color: var(--vscode-peekViewTitle-background); - --bs-btn-hover-border-color: var(--vscode-peekViewTitle-background); - --bs-btn-color: var(--vscode-peekViewTitleDescription-foreground); - --bs-btn-hover-color: var(--vscode-peekViewTitleDescription-foreground); -} - -body[class^="vscode-"] .btn-primary { - --bs-btn-bg: var(--vscode-button-background); - --bs-btn-border-color: var(--vscode-button-background); - --bs-btn-hover-bg: var(--vscode-button-hoverBackground); - --bs-btn-hover-border-color: var(--vscode-button-hoverBackground); - --bs-btn-color: var(--vscode-button-foreground); - --bs-btn-hover-color: var(--vscode-button-foreground); - --bs-btn-disabled-color: var(--vscode-button-foreground); - --bs-btn-disabled-bg: var(--vscode-button-background); - --bs-btn-disabled-border-color: var(--vscode-button-background); - --bs-btn-disabled-opacity: 0.8; -} - -body[class^="vscode-"] .navbar-brand { - --bs-navbar-brand-color: var(--vscode-sideBarSectionHeader-foreground); - --bs-navbar-brand-hover-color: var(--vscode-sideBarSectionHeader-foreground); -} - -body[class^="vscode-"] .navbar-text { - --bs-navbar-color: var(--vscode-sideBarSectionHeader-foreground); -} - -body[class^="vscode-"] .accordion-item { - --bs-accordion-active-bg: var(--vscode-list-inactiveSelectionBackground); -} - -body[class^="vscode-"] .card-header { - --bs-border-color: var(--vscode-editorGroup-border); - --bs-card-border-color: var(--vscode-editorGroup-border); -} - -body[class^="vscode-"] .card { - --bs-border-color: var(--vscode-editorGroup-border); - --bs-card-border-color: var(--vscode-editorGroup-border); -} - -body[class^="vscode-"] .nav-pills { - --bs-nav-pills-link-active-bg: var(--vscode-notifications-background); - --bs-nav-pills-link-active-color: var(--vscode-notifications-foreground); -} - -body[class^="vscode-"] .nav-link { - --bs-nav-link-color: var(--vscode-notificationLink-foreground); - --bs-link-hover-color: var(--vscode-notificationLink-foreground); -} - -body[class^="vscode-"] .nav-link:hover { - --bs-nav-link-color: var(--vscode-notificationLink-foreground); - --bs-nav-link-hover-color: var(--vscode-notificationLink-foreground); - --bs-nav-tabs-link-hover-border-color: var( - --vscode-notificationLink-foreground - ); -} - -body[class^="vscode-"] .ansi-display { - --ansiBlack: var(--vscode-terminal-ansiBlack); - --ansiRed: var(--vscode-terminal-ansiRed); - --ansiGreen: var(--vscode-terminal-ansiGreen); - --ansiYellow: var(--vscode-terminal-ansiYellow); - --ansiBlue: var(--vscode-terminal-ansiBlue); - --ansiMagenta: var(--vscode-terminal-ansiMagenta); - --ansiCyan: var(--vscode-terminal-ansiCyan); - --ansiWhite: var(--vscode-terminal-ansiWhite); - --ansiBrightBlack: var(--vscode-terminal-ansiBrightBlack); - --ansiBrightRed: var(--vscode-terminal-ansiBrightRed); - --ansiBrightGreen: var(--vscode-terminal-ansiBrightGreen); - --ansiBrightYellow: var(--vscode-terminal-ansiBrightYellow); - --ansiBrightBlue: var(--vscode-terminal-ansiBrightBlue); - --ansiBrightMagenta: var(--vscode-terminal-ansiBrightMagenta); - --ansiBrightCyan: var(--vscode-terminal-ansiBrightCyan); - --ansiBrightWhite: var(--vscode-terminal-ansiBrightWhite); -} - -body[class^="vscode-"] .sidebar .list-group { - --bs-tertiary-bg: var(--vscode-list-hoverBackground); - --bs-secondary-color: var(--vscode-foreground); - --bs-list-group-active-bg: var(--vscode-sideBarSectionHeader-background); - --bs-list-group-active-border-color: var( - --vscode-sideBarSectionHeader-background - ); - --bs-list-group-action-active-bg: var( - --vscode-sideBarSectionHeader-background - ); - --bs-list-group-active-color: var(--vscode-sideBarSectionHeader-foreground); -} - -body[class^="vscode-"] .breadcrumb-item { - --bs-breadcrumb-divider-color: var(--vscode-foreground); -} - -body[class^="vscode-"] div.ap-control-bar .ap-fullscreen-button { - display: none; -} - -:root { - --bs-navbar-padding-y: 0; - --bs-navbar-brand-padding-y: 0; - --sidebar-width: 550px; -} - -body { - margin: 0; - padding: 0; -} - -.navbar { - padding-top: 0; - padding-bottom: 0; - background-color: var(--bs-light); - top: 0; -} - -.navbar-title-grid { - display: grid; - grid-template-columns: minmax(350px, 1fr) 1fr; - width: 100%; -} - -@media (max-width: 575px) { - .navbar .vertical-metric-label { - font-size: 0.7rem !important; - } -} - -@media (max-width: 575px) { - .tab-tools select { - width: 50px; - } -} - -@media (max-width: 575px) { - .navbar .vertical-metric-value { - margin-top: 0.2rem !important; - font-size: 0.9rem !important; - } -} - -@media (max-width: 575px) { - .navbar-title-grid { - grid-template-columns: 1fr auto-fill; - } -} - -[data-bs-theme="dark"] .navbar { - background-color: unset; -} - -.navbar-brand { - font-weight: 400; - font-size: 1.4em; -} - -.navbar-text { - padding-top: 0px; - padding-bottom: 0px; -} - -#sidebarToggle > i.bi { - font-size: 1.5em; -} - -.nav-link.active { - border-bottom-width: 0 !important; -} - -.workspace { - display: flex; - flex-direction: column; -} - -.workspace.full-screen { - top: 0; -} - -@media (min-width: 768px) { - .workspace { - left: var(--sidebar-width); - } - .workspace.full-screen { - left: 0; - } - .workspace.off-canvas { - left: 0; - } -} - -.no-last-para-padding > p:last-of-type { - margin-bottom: 0; -} - -.sidebar .list-group-item { - cursor: pointer; - border-left-width: none; - border-top: none; - border-right: none; - border-radius: 0; -} - -.btn-tools { - --bs-btn-bg: var(--bs-secondary-bg-subtle); - --bs-btn-hover-bg: var(--bs-secondary-bg-subtle); - --bs-btn-border-color: var(--bs-secondary-bg-subtle); - --bs-btn-hover-border-color: var(--bs-secondary-bg-subtle); -} - -.sidebar .list-group { - --bs-list-group-active-color: var(--bs-gray-700); - --bs-list-group-active-bg: var(--bs-gray-200); - --bs-list-group-active-border-color: var(--bs-gray-200); -} - -[data-bs-theme="dark"] .sidebar .list-group { - --bs-list-group-active-color: var(--bs-gray-300); - --bs-list-group-active-bg: var(--bs-gray-800); - --bs-list-group-active-border-color: var(--bs-gray-800); -} - -.markdown-content pre > code, -.markdown-content pre { - white-space: pre-wrap !important; - word-wrap: anywhere !important; -} - -.log pre code { - white-space: pre-wrap; - font-size: 0.9em; -} - -.log pre[class*="language-"] { - margin: 0; - padding: 0.3em; -} - -.log :not(pre) > code[class*="language-"], -.log pre[class*="language-"] { - background-color: var(--bs-body-background); -} - -.workspace pre[class*="language-"] { - margin: 0; - padding: 0.3em; -} - -.workspace :not(pre) > code[class*="language-"], -.workspace pre[class*="language-"] { - background-color: var(--bs-body-background); -} - -.tbd { - font-weight: 300; - opacity: 0.9; - width: 100%; - text-align: center; - padding-top: 10em; -} - -code:not(.sourceCode):not(.source-code):not([class^="language-"]) { - color: var(--bs-code-color); -} - -.token.attr-name, -.token.builtin, -.token.char, -.token.inserted, -.token.selector, -.token.string { - color: var(--bs-body-color); -} - -.token.operator { - background: inherit; -} - -pre[class*="language-"] { - border: unset; - border-radius: unset; - box-shadow: unset; -} - -.font-title { - font-size: 1.5rem; - font-weight: 600; -} - -.font-subtitle { - font-size: 1.1rem; - font-weight: 200; -} - -.tight-paragraphs p { - margin-bottom: 0; -} - -.tight-last-paragraph p:last-of-type { - margin-bottom: 0; -} - -.card { - margin-bottom: 0.5em; -} - -.card-header { - padding: 0.1em 1em 0.1em 1em; - font-size: 0.8rem; - font-weight: 500; -} - -.btn .btn-link { - cursor: pointer; -} - -[aria-expanded="false"] .hide-when-collapsed { - display: none; -} - -[aria-expanded="true"] .hide-when-expanded { - opacity: 0 !important; -} - -[aria-expanded="true"] .no-bottom-padding-when-expanded { - padding-bottom: 0 !important; -} - -[aria-expanded="true"] .zerowidth-when-expanded { - height: 0px !important; - width: 0px !important; - padding-left: 0 !important; - padding-right: 0 !important; - margin-left: 0 !important; - margin-right: 0 !important; -} - -[aria-expanded="true"] .zeroheight-when-expanded { - height: 0px !important; -} - -.accordion-item:not(.no-highlight) .accordion-button:not(.collapsed) { - border-left: solid var(--bs-accordion-active-bg) 2px; - border-right: solid var(--bs-accordion-active-bg) 2px; - border-top: solid var(--bs-accordion-active-bg) 2px; - background-color: var(--bs-body-background); - color: var(--bs-body-color); -} - -.accordion-item .accordion-button { - border-left: solid var(--bs-body-bg) 2px; - border-right: solid var(--bs-body-bg) 2px; - border-top: solid var(--bs-body-bg) 2px; - background-color: var(--bs-body-bg); - color: var(--bs-body-color); -} - -.accordion-button[aria-expanded="true"] .giant-text-when-expanded { - font-weight: 500; - font-size: 0.9rem !important; - flex-grow: 40; - padding-top: 0rem; - padding-bottom: 0rem; -} - -.accordion-button[aria-expanded="true"] .full-flex-basis-when-expanded { - flex-basis: content !important; - flex-shrink: 1; -} - -.markdown-content h1, -.markdown-content h2, -.markdown-content h3, -.markdown-content h4, -.markdown-content h5, -.markdown-content h6 { - font-size: 0.9em; - font-weight: 600; - margin-top: 0.25em; - margin-bottom: 0.25em; -} - -.sample-answer .markdown-content h1, -.sample-answer .markdown-content h2, -.sample-answer .markdown-content h3, -.sample-answer .markdown-content h4, -.sample-answer .markdown-content h5, -.sample-answer .markdown-content h6 { - font-size: 1em; - font-weight: 400; - margin-top: 0em; - margin-bottom: 0em; -} - -.accordion-item:not(.no-highlight) .collapse.show.highlight-when-expanded, -.accordion-item:not(.no-highlight) .collapsing.highlight-when-expanded, -.accordion-item:not(.no-highlight) - .accordion-button[aria-expanded="true"].highlight-when-expanded { - border-left: solid var(--bs-accordion-active-bg) 2px; - border-right: solid var(--bs-accordion-active-bg) 2px; - border-bottom: solid var(--bs-accordion-active-bg) 2px; -} - -.accordion-item.no-highlight .accordion-button { - background-color: var(--bs-body-bg); -} - -.accordion-button.toggle-rotated:after { - background-size: 0.8em; - height: 0.8em; - width: 0.8em; -} - -.accordion-header > :last-child::after, -.accordion-button:last-child:after { - color: white !important; -} - -.card .accordion-button.toggle-rotated:after { - margin-right: 1rem; -} - -.no-highlight, -.no-highlight:hover, -.no-highlight:active { - color: inherit; -} - -.toggle-rotated:before { - transition: transform 0.3s linear; -} - -[aria-expanded="false"] .toggle-rotated:before { - transform: none; -} - -[aria-expanded="true"] .toggle-rotated:before { - transform: rotate(90deg); -} - -.fadeout-when-not-collapsed:not(.collapsed) { - opacity: 0 !important; -} - -table.table { - width: unset; -} - -.table > :not(caption) > * > * { - background-color: unset; -} - -table.table.table-sm td { - padding: 0.1em; -} - -.popover.rendered-content { - display: flex; - flex-direction: column; - max-height: 80%; - max-width: 80%; - /** Do not define an overflow on the wrapper or your arrow to the element will disapear */ -} - -.popover.rendered-content .popover-header { - font-size: 0.8em; - font-weight: 400; -} - -.popover.rendered-content .popover-body { - overflow: auto; -} - -.modal-footer button { - padding: 0.2rem 0.5rem; - font-size: 0.8em; -} - -.do-not-collapse-self { - display: block; - height: auto !important; -} - -[data-tooltip] { - position: relative; -} -[data-tooltip]:hover::after { - content: attr(data-tooltip); - position: absolute; - line-height: 1.25; - background: var(--bs-light); - color: var(--bs-body-color); - opacity: 1; - padding: 4px 8px; - border-radius: 4px; - border: 1px solid var(--bs-border-color); - box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.25); - white-space: pre-wrap; - width: max-content; - max-width: 400px; - z-index: 1000; -} -[data-tooltip][data-tooltip-position="bottom-left"]:hover::after { - right: 0%; - top: 100%; -} - -/* ANSI Coloring */ -.ansi-display { - font-family: monospace; - white-space: pre-wrap; - --ansiBlack: #000000; - --ansiRed: #cd3131; - --ansiGreen: #00bc00; - --ansiYellow: #949800; - --ansiBlue: #0451a5; - --ansiMagenta: #bc05bc; - --ansiCyan: #0598bc; - --ansiWhite: #555555; - --ansiBrightBlack: #666666; - --ansiBrightRed: #cd3131; - --ansiBrightGreen: #14ce14; - --ansiBrightYellow: #b5ba00; - --ansiBrightBlue: #0451a5; - --ansiBrightMagenta: #bc05bc; - --ansiBrightCyan: #0598bc; - --ansiBrightWhite: #a5a5a5; -} - -@keyframes ansi-display-run-blink { - 50% { - opacity: 0; - } -} - -.tab-tools .form-select { - background-position: right 0.3rem center; - background-size: 10px 10px; - padding: 0.1rem 20px 0.1rem 10px; -} - -.tab-content > .active { - display: flex; -} - -.left-to-right-animate { - position: absolute; - animation: moveLeftToRight 2s linear infinite; -} - -@keyframes moveLeftToRight { - from { - margin-left: 0; - } - to { - margin-left: 95%; - } -} - -.expandable-panel pre { - overflow: unset; -} - -.markdown-content pre[class*="language-"], -pre[class*="language-"].tool-output, -.tool-output { - background-color: #f8f8f8; -} - -.vscode-dark .model-call pre[class*="language-"], -.vscode-dark .markdown-content pre[class*="language-"], -.vscode-dark pre[class*="language-"].tool-output, -.vscode-dark .tool-output { - background-color: #333333; -} - -.model-call pre[class*="language-"], -.markdown-content pre[class*="language-"], -pre[class*="language-"].tool-output { - border: none !important; - box-shadow: none !important; - border-radius: var(--bs-border-radius) !important; -} - -.vscode-dark pre.jsonPanel { - background: none !important; - border: none !important; - box-shadow: none !important; - border-radius: var(--bs-border-radius) !important; -} - -/* lightbox styles */ - -.lightbox-overlay .close-button, -.lightbox-overlay .nav-button { - /* Hide by default */ - opacity: 0; - pointer-events: none; /* so it doesn't register clicks when hidden */ - transition: opacity 0.3s ease; -} - -.lightbox-overlay:hover .close-button, -.lightbox-overlay .nav-button { - /* Show on hover */ - opacity: 1; - pointer-events: auto; -} - -/* jsondiffpatch */ - -.jsondiffpatch-delta { - padding: 1em; - background: var(--bs-light); - font-family: var(--bs-font-monospace); - font-size: 0.9em; -} -.jsondiffpatch-delta pre { - white-space: pre-wrap; - word-wrap: break-word; - word-break: break-all; - margin-bottom: 0; -} -ul.jsondiffpatch-delta { - list-style-type: none; - padding: 0 0 0 1.5em; - margin: 0; -} -.jsondiffpatch-delta ul { - list-style-type: none; - padding: 0 0 0 1.5em; - margin: 0; -} -.jsondiffpatch-added, -.jsondiffpatch-modified .jsondiffpatch-right-value pre, -.jsondiffpatch-textdiff-added { - background: var(--inspect-diff-add-color); -} - -.jsondiffpatch-deleted .jsondiffpatch-property-name, -.jsondiffpatch-deleted pre, -.jsondiffpatch-modified .jsondiffpatch-left-value pre, -.jsondiffpatch-textdiff-deleted { - background: var(--inspect-diff-remove-color); - text-decoration: line-through; -} -.jsondiffpatch-unchanged, -.jsondiffpatch-movedestination { - color: gray; -} -.jsondiffpatch-unchanged, -.jsondiffpatch-movedestination > .jsondiffpatch-value { - transition: all 0.5s; - -webkit-transition: all 0.5s; - overflow-y: hidden; -} -.jsondiffpatch-unchanged-showing .jsondiffpatch-unchanged, -.jsondiffpatch-unchanged-showing - .jsondiffpatch-movedestination - > .jsondiffpatch-value { - max-height: 100px; -} -.jsondiffpatch-unchanged-hidden .jsondiffpatch-unchanged, -.jsondiffpatch-unchanged-hidden - .jsondiffpatch-movedestination - > .jsondiffpatch-value { - max-height: 0; -} -.jsondiffpatch-unchanged-hiding - .jsondiffpatch-movedestination - > .jsondiffpatch-value, -.jsondiffpatch-unchanged-hidden - .jsondiffpatch-movedestination - > .jsondiffpatch-value { - display: block; -} -.jsondiffpatch-unchanged-visible .jsondiffpatch-unchanged, -.jsondiffpatch-unchanged-visible - .jsondiffpatch-movedestination - > .jsondiffpatch-value { - max-height: 100px; -} -.jsondiffpatch-unchanged-hiding .jsondiffpatch-unchanged, -.jsondiffpatch-unchanged-hiding - .jsondiffpatch-movedestination - > .jsondiffpatch-value { - max-height: 0; -} -.jsondiffpatch-unchanged-showing .jsondiffpatch-arrow, -.jsondiffpatch-unchanged-hiding .jsondiffpatch-arrow { - display: none; -} -.jsondiffpatch-value { - display: inline-block; -} -.jsondiffpatch-property-name { - display: inline-block; - padding-right: 5px; - vertical-align: top; -} -.jsondiffpatch-property-name:after { - content: ": "; -} -.jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after { - content: ": ["; -} -.jsondiffpatch-child-node-type-array:after { - content: "],"; -} -div.jsondiffpatch-child-node-type-array:before { - content: "["; -} -div.jsondiffpatch-child-node-type-array:after { - content: "]"; -} -.jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after { - content: ": {"; -} -.jsondiffpatch-child-node-type-object:after { - content: "},"; -} -div.jsondiffpatch-child-node-type-object:before { - content: "{"; -} -div.jsondiffpatch-child-node-type-object:after { - content: "}"; -} -.jsondiffpatch-value pre:after { - content: ","; -} -li:last-child > .jsondiffpatch-value pre:after, -.jsondiffpatch-modified > .jsondiffpatch-left-value pre:after { - content: ""; -} -.jsondiffpatch-modified .jsondiffpatch-value { - display: inline-block; -} -.jsondiffpatch-modified .jsondiffpatch-right-value { - margin-left: 0; -} -.jsondiffpatch-moved .jsondiffpatch-value { - display: none; -} -.jsondiffpatch-moved .jsondiffpatch-moved-destination { - display: inline-block; - background: #ffffbb; - color: #888; -} -.jsondiffpatch-moved .jsondiffpatch-moved-destination:before { - content: " => "; -} -ul.jsondiffpatch-textdiff { - padding: 0; -} -.jsondiffpatch-textdiff-location { - color: #bbb; - display: inline-block; - min-width: 60px; -} -.jsondiffpatch-textdiff-line { - display: inline-block; -} -.jsondiffpatch-textdiff-line-number:after { - content: ","; -} -.jsondiffpatch-error { - background: red; - color: white; - font-weight: bold; -} - -/* prism-custom.css */ -code[class*="language-"], -pre[class*="language-"] { - font-size: 0.7rem !important; -} - -.token { - font-size: 0.7rem !important; -} - -/* PrismJS 1.29.0 https://prismjs.com/download.html#themes=prism-dark&languages=markup+css+clike+javascript+bash+python */ -/* This has been generated from the URL above, then scoped within the - * .vscode-dark class. If it needs to be regenerated, be sure to add that back in. */ -.vscode-dark code[class*="language-"], -.vscode-dark pre[class*="language-"] { - color: #fff; - background: 0 0; - text-shadow: 0 -0.1em 0.2em #000; - font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; - font-size: 1em; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} -@media print { - .vscode-dark code[class*="language-"], - .vscode-dark pre[class*="language-"] { - text-shadow: none; - } -} -.vscode-dark :not(pre) > code[class*="language-"], -.vscode-dark pre[class*="language-"] { - background: #4c3f33; -} -.vscode-dark pre[class*="language-"] { - padding: 1em; - margin: 0.5em 0; - overflow: auto; - /* border: 0.3em solid #7a6651; */ - border-radius: 0.5em; - box-shadow: 1px 1px 0.5em #000 inset; -} -.vscode-dark :not(pre) > code[class*="language-"] { - padding: 0.15em 0.2em 0.05em; - border-radius: 0.3em; - /* border: 0.13em solid #7a6651; */ - box-shadow: 1px 1px 0.3em -0.1em #000 inset; - white-space: normal; -} -.vscode-dark .token.cdata, -.vscode-dark .token.comment, -.vscode-dark .token.doctype, -.vscode-dark .token.prolog { - color: #997f66; -} -.vscode-dark .token.punctuation { - opacity: 0.7; -} -.vscode-dark .token.namespace { - opacity: 0.7; -} -.vscode-dark .token.boolean, -.vscode-dark .token.constant, -.vscode-dark .token.number, -.vscode-dark .token.property, -.vscode-dark .token.symbol, -.vscode-dark .token.tag { - color: #d1939e; -} -.vscode-dark .token.attr-name, -.vscode-dark .token.builtin, -.vscode-dark .token.char, -.vscode-dark .token.inserted, -.vscode-dark .token.selector, -.vscode-dark .token.string { - color: #bce051; -} -.vscode-dark .language-css .token.string, -.vscode-dark .style .token.string, -.vscode-dark .token.entity, -.vscode-dark .token.operator, -.vscode-dark .token.url, -.vscode-dark .token.variable { - color: #f4b73d; -} -.vscode-dark .token.atrule, -.vscode-dark .token.attr-value, -.vscode-dark .token.keyword { - color: #d1939e; -} -.vscode-dark .token.important, -.vscode-dark .token.regex { - color: #e90; -} -.vscode-dark .token.bold, -.vscode-dark .token.important { - font-weight: 700; -} -.vscode-dark .token.italic { - font-style: italic; -} -.vscode-dark .token.entity { - cursor: help; -} -.vscode-dark .token.deleted { - color: red; -} -/* END PrismJS */ - -/* SVG Icon styles - following Bootstrap Icons pattern */ -.ii::before { - background-color: currentColor; -} - -.inspect-icon-16::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 12.33 12.33%22%3E %3Cg id%3D%22Layer_2%22%3E %3Cpath class%3D%22cls-1%22 d%3D%22M5.05%2C12.16c-.14-.11-.24-.27-.29-.48l-.26-1.12-.28-.1-.98.61c-.18.11-.36.16-.54.13s-.35-.11-.5-.25l-.82-.82c-.15-.15-.24-.31-.26-.5s.03-.36.13-.54l.62-.98-.1-.27-1.12-.27c-.2-.05-.36-.14-.48-.29s-.17-.32-.17-.52v-1.17c0-.21.06-.38.17-.52s.27-.24.48-.29l1.12-.26.11-.29-.62-.98c-.11-.17-.16-.35-.14-.53s.11-.35.25-.5l.82-.83c.15-.14.31-.22.5-.25s.36.02.54.13l.98.62.29-.11.26-1.13c.05-.2.14-.36.29-.48s.32-.17.53-.17h1.18c.21%2C0%2C.38.06.52.17s.24.27.28.48l.26%2C1.13.29.11.98-.62c.17-.11.35-.15.53-.13s.35.11.49.25l.83.83c.14.15.23.31.25.5s-.02.36-.13.53l-.62.98.11.29%2C1.12.26c.2.05.36.15.48.29s.17.32.17.52v1.17c0%2C.2-.06.38-.17.52s-.27.24-.48.29l-1.12.27-.11.27.62.98c.11.18.15.36.13.54s-.11.35-.25.5l-.83.82c-.14.14-.31.23-.49.25s-.36-.02-.54-.13l-.98-.61-.28.1-.26%2C1.12c-.05.21-.14.37-.28.48s-.32.17-.52.17h-1.18c-.21%2C0-.38-.06-.53-.17ZM6.69%2C11.63c.14%2C0%2C.22-.07.24-.2l.34-1.42c.16-.04.31-.09.46-.16s.28-.12.4-.19l1.24.77c.11.07.22.06.32-.04l.71-.71c.09-.09.1-.2.03-.32l-.77-1.24c.06-.12.12-.25.18-.4s.11-.3.16-.46l1.43-.33c.13-.03.2-.11.2-.25v-1.02c0-.13-.07-.21-.2-.25l-1.42-.33c-.04-.17-.1-.33-.16-.48s-.12-.28-.18-.39l.77-1.24c.07-.12.06-.23-.03-.32l-.71-.71c-.1-.1-.21-.11-.33-.04l-1.24.77c-.12-.06-.25-.12-.4-.18s-.3-.11-.47-.16l-.34-1.43c-.02-.13-.1-.2-.24-.2h-1.03c-.14%2C0-.22.07-.25.2l-.33%2C1.42c-.16.04-.31.1-.47.16s-.29.12-.41.19l-1.24-.76c-.11-.07-.22-.06-.32.04l-.72.71c-.09.09-.1.2-.03.32l.77%2C1.24c-.05.11-.1.24-.17.39s-.12.31-.16.48l-1.42.33c-.13.03-.19.11-.19.25v1.02c0%2C.14.06.22.19.25l1.43.33c.04.16.1.31.16.46s.12.28.18.4l-.77%2C1.24c-.07.12-.06.22.04.32l.71.71c.11.1.21.12.32.04l1.24-.77c.12.07.26.13.4.19s.3.11.47.16l.33%2C1.42c.03.13.11.2.25.2h1.03Z%22%2F%3E %3C%2Fg%3E %3Cg id%3D%22Layer_3%22%3E %3Crect class%3D%22cls-1%22 x%3D%225.41%22 y%3D%225.36%22 width%3D%221.5%22 height%3D%223.75%22 rx%3D%22.34%22 ry%3D%22.34%22%2F%3E %3Ccircle class%3D%22cls-1%22 cx%3D%226.12%22 cy%3D%224%22 r%3D%22.92%22%2F%3E %3C%2Fg%3E%3C%2Fsvg%3E") no-repeat center / contain; - -webkit-mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 12.33 12.33%22%3E %3Cg id%3D%22Layer_2%22%3E %3Cpath class%3D%22cls-1%22 d%3D%22M5.05%2C12.16c-.14-.11-.24-.27-.29-.48l-.26-1.12-.28-.1-.98.61c-.18.11-.36.16-.54.13s-.35-.11-.5-.25l-.82-.82c-.15-.15-.24-.31-.26-.5s.03-.36.13-.54l.62-.98-.1-.27-1.12-.27c-.2-.05-.36-.14-.48-.29s-.17-.32-.17-.52v-1.17c0-.21.06-.38.17-.52s.27-.24.48-.29l1.12-.26.11-.29-.62-.98c-.11-.17-.16-.35-.14-.53s.11-.35.25-.5l.82-.83c.15-.14.31-.22.5-.25s.36.02.54.13l.98.62.29-.11.26-1.13c.05-.2.14-.36.29-.48s.32-.17.53-.17h1.18c.21%2C0%2C.38.06.52.17s.24.27.28.48l.26%2C1.13.29.11.98-.62c.17-.11.35-.15.53-.13s.35.11.49.25l.83.83c.14.15.23.31.25.5s-.02.36-.13.53l-.62.98.11.29%2C1.12.26c.2.05.36.15.48.29s.17.32.17.52v1.17c0%2C.2-.06.38-.17.52s-.27.24-.48.29l-1.12.27-.11.27.62.98c.11.18.15.36.13.54s-.11.35-.25.5l-.83.82c-.14.14-.31.23-.49.25s-.36-.02-.54-.13l-.98-.61-.28.1-.26%2C1.12c-.05.21-.14.37-.28.48s-.32.17-.52.17h-1.18c-.21%2C0-.38-.06-.53-.17ZM6.69%2C11.63c.14%2C0%2C.22-.07.24-.2l.34-1.42c.16-.04.31-.09.46-.16s.28-.12.4-.19l1.24.77c.11.07.22.06.32-.04l.71-.71c.09-.09.1-.2.03-.32l-.77-1.24c.06-.12.12-.25.18-.4s.11-.3.16-.46l1.43-.33c.13-.03.2-.11.2-.25v-1.02c0-.13-.07-.21-.2-.25l-1.42-.33c-.04-.17-.1-.33-.16-.48s-.12-.28-.18-.39l.77-1.24c.07-.12.06-.23-.03-.32l-.71-.71c-.1-.1-.21-.11-.33-.04l-1.24.77c-.12-.06-.25-.12-.4-.18s-.3-.11-.47-.16l-.34-1.43c-.02-.13-.1-.2-.24-.2h-1.03c-.14%2C0-.22.07-.25.2l-.33%2C1.42c-.16.04-.31.1-.47.16s-.29.12-.41.19l-1.24-.76c-.11-.07-.22-.06-.32.04l-.72.71c-.09.09-.1.2-.03.32l.77%2C1.24c-.05.11-.1.24-.17.39s-.12.31-.16.48l-1.42.33c-.13.03-.19.11-.19.25v1.02c0%2C.14.06.22.19.25l1.43.33c.04.16.1.31.16.46s.12.28.18.4l-.77%2C1.24c-.07.12-.06.22.04.32l.71.71c.11.1.21.12.32.04l1.24-.77c.12.07.26.13.4.19s.3.11.47.16l.33%2C1.42c.03.13.11.2.25.2h1.03Z%22%2F%3E %3C%2Fg%3E %3Cg id%3D%22Layer_3%22%3E %3Crect class%3D%22cls-1%22 x%3D%225.41%22 y%3D%225.36%22 width%3D%221.5%22 height%3D%223.75%22 rx%3D%22.34%22 ry%3D%22.34%22%2F%3E %3Ccircle class%3D%22cls-1%22 cx%3D%226.12%22 cy%3D%224%22 r%3D%22.92%22%2F%3E %3C%2Fg%3E%3C%2Fsvg%3E") no-repeat center / contain; -} - -.inspect-icon-back::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg id%3D%22Layer_2%22 xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 5.77 9.73%22%3E %3Crect class%3D%22cls-1%22 x%3D%22-.82%22 y%3D%221.97%22 width%3D%227.07%22 height%3D%221.5%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(-1.13 2.72) rotate(-45)%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%222.14%22 y%3D%223.08%22 width%3D%221.5%22 height%3D%227.54%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(-4 4.05) rotate(-45)%22%2F%3E%3C%2Fsvg%3E") no-repeat center / contain; - -webkit-mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg id%3D%22Layer_2%22 xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 5.77 9.73%22%3E %3Crect class%3D%22cls-1%22 x%3D%22-.82%22 y%3D%221.97%22 width%3D%227.07%22 height%3D%221.5%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(-1.13 2.72) rotate(-45)%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%222.14%22 y%3D%223.08%22 width%3D%221.5%22 height%3D%227.54%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(-4 4.05) rotate(-45)%22%2F%3E%3C%2Fsvg%3E") no-repeat center / contain; -} - -.inspect-icon-forward::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg id%3D%22Layer_2%22 xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 5.77 9.73%22%3E %3Cg transform%3D%22scale(-1%2C 1) translate(-5.77%2C 0)%22%3E %3Crect class%3D%22cls-1%22 x%3D%22-.82%22 y%3D%221.97%22 width%3D%227.07%22 height%3D%221.5%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(-1.13 2.72) rotate(-45)%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%222.14%22 y%3D%223.08%22 width%3D%221.5%22 height%3D%227.54%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(-4 4.05) rotate(-45)%22%2F%3E %3C%2Fg%3E%3C%2Fsvg%3E") no-repeat center / contain; - -webkit-mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg id%3D%22Layer_2%22 xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 5.77 9.73%22%3E %3Cg transform%3D%22scale(-1%2C 1) translate(-5.77%2C 0)%22%3E %3Crect class%3D%22cls-1%22 x%3D%22-.82%22 y%3D%221.97%22 width%3D%227.07%22 height%3D%221.5%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(-1.13 2.72) rotate(-45)%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%222.14%22 y%3D%223.08%22 width%3D%221.5%22 height%3D%227.54%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(-4 4.05) rotate(-45)%22%2F%3E %3C%2Fg%3E%3C%2Fsvg%3E") no-repeat center / contain; -} - -.inspect-icon-file::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 12 15%22%3E %3Cdefs%3E %3Cstyle%3E .cls-1 %7B fill%3A none%3B stroke%3A currentColor%3B stroke-miterlimit%3A 10%3B %7D .cls-2 %7B fill%3A currentColor%3B %7D %3C%2Fstyle%3E %3C%2Fdefs%3E %3Cg id%3D%22Layer_1%22%3E %3Cpath class%3D%22cls-2%22 d%3D%22M5.26%2C11.55c-.1-.07-.16-.18-.19-.32l-.17-.74-.19-.07-.65.4c-.12.07-.24.1-.36.09s-.23-.07-.33-.17l-.54-.54c-.1-.1-.16-.21-.17-.33s.02-.24.09-.36l.41-.65-.07-.18-.74-.18c-.13-.03-.24-.09-.32-.19s-.11-.21-.11-.35v-.77c0-.14.04-.25.11-.35s.18-.16.32-.19l.74-.17.07-.19-.41-.65c-.07-.11-.11-.23-.09-.35s.07-.23.17-.33l.54-.55c.1-.09.21-.15.33-.16s.24.01.35.09l.65.41.19-.07.17-.75c.03-.13.09-.24.19-.32s.21-.11.35-.11h.78c.14%2C0%2C.25.04.35.11s.16.18.19.32l.17.75.19.07.65-.41c.11-.07.23-.1.35-.09s.23.07.33.16l.55.55c.1.1.15.21.17.33s-.01.24-.09.35l-.41.65.07.19.74.17c.13.03.24.1.32.19s.11.21.11.35v.77c0%2C.13-.04.25-.11.35s-.18.16-.32.19l-.74.18-.07.18.41.65c.07.12.1.24.09.36s-.07.23-.17.33l-.55.54c-.1.1-.2.15-.33.17s-.24-.01-.36-.09l-.65-.4-.19.07-.17.74c-.03.14-.09.24-.19.32s-.21.11-.35.11h-.78c-.14%2C0-.25-.04-.35-.11ZM6.34%2C11.2c.09%2C0%2C.14-.04.16-.13l.23-.94c.11-.03.21-.06.31-.1s.19-.08.26-.13l.82.51c.07.05.14.04.21-.03l.47-.47c.06-.06.07-.13.02-.21l-.51-.82c.04-.08.08-.17.12-.27s.07-.2.1-.3l.95-.22c.09-.02.13-.07.13-.16v-.67c0-.09-.04-.14-.13-.16l-.94-.22c-.03-.11-.06-.22-.11-.32s-.08-.18-.12-.26l.51-.82c.05-.08.04-.15-.02-.21l-.47-.47c-.07-.06-.14-.07-.22-.03l-.82.51c-.08-.04-.16-.08-.26-.12s-.2-.08-.31-.11l-.23-.95c-.02-.09-.07-.13-.16-.13h-.68c-.09%2C0-.14.04-.16.13l-.22.94c-.1.03-.21.06-.31.1s-.19.08-.27.12l-.82-.5c-.07-.05-.15-.04-.21.03l-.48.47c-.06.06-.07.13-.02.21l.51.82c-.03.07-.07.16-.11.26s-.08.2-.11.32l-.94.22c-.09.02-.13.07-.13.16v.67c0%2C.09.04.14.13.16l.95.22c.03.11.06.21.1.3s.08.19.12.27l-.51.82c-.05.08-.04.15.02.21l.47.47c.07.07.14.08.21.03l.82-.51c.08.04.17.09.27.13s.2.07.31.1l.22.94c.02.09.07.13.16.13h.68Z%22%2F%3E %3C%2Fg%3E %3Cg id%3D%22Layer_3%22%3E %3Crect class%3D%22cls-2%22 x%3D%225.46%22 y%3D%227.01%22 width%3D%221.07%22 height%3D%222.68%22 rx%3D%22.34%22 ry%3D%22.34%22%2F%3E %3Ccircle class%3D%22cls-2%22 cx%3D%226%22 cy%3D%226.04%22 r%3D%22.65%22%2F%3E %3C%2Fg%3E %3Cg id%3D%22Layer_2%22%3E %3Crect class%3D%22cls-1%22 x%3D%22.5%22 y%3D%22.5%22 width%3D%2211%22 height%3D%2214%22 rx%3D%221%22 ry%3D%221%22%2F%3E %3C%2Fg%3E%3C%2Fsvg%3E") no-repeat center / contain; - -webkit-mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 12 15%22%3E %3Cdefs%3E %3Cstyle%3E .cls-1 %7B fill%3A none%3B stroke%3A currentColor%3B stroke-miterlimit%3A 10%3B %7D .cls-2 %7B fill%3A currentColor%3B %7D %3C%2Fstyle%3E %3C%2Fdefs%3E %3Cg id%3D%22Layer_1%22%3E %3Cpath class%3D%22cls-2%22 d%3D%22M5.26%2C11.55c-.1-.07-.16-.18-.19-.32l-.17-.74-.19-.07-.65.4c-.12.07-.24.1-.36.09s-.23-.07-.33-.17l-.54-.54c-.1-.1-.16-.21-.17-.33s.02-.24.09-.36l.41-.65-.07-.18-.74-.18c-.13-.03-.24-.09-.32-.19s-.11-.21-.11-.35v-.77c0-.14.04-.25.11-.35s.18-.16.32-.19l.74-.17.07-.19-.41-.65c-.07-.11-.11-.23-.09-.35s.07-.23.17-.33l.54-.55c.1-.09.21-.15.33-.16s.24.01.35.09l.65.41.19-.07.17-.75c.03-.13.09-.24.19-.32s.21-.11.35-.11h.78c.14%2C0%2C.25.04.35.11s.16.18.19.32l.17.75.19.07.65-.41c.11-.07.23-.1.35-.09s.23.07.33.16l.55.55c.1.1.15.21.17.33s-.01.24-.09.35l-.41.65.07.19.74.17c.13.03.24.1.32.19s.11.21.11.35v.77c0%2C.13-.04.25-.11.35s-.18.16-.32.19l-.74.18-.07.18.41.65c.07.12.1.24.09.36s-.07.23-.17.33l-.55.54c-.1.1-.2.15-.33.17s-.24-.01-.36-.09l-.65-.4-.19.07-.17.74c-.03.14-.09.24-.19.32s-.21.11-.35.11h-.78c-.14%2C0-.25-.04-.35-.11ZM6.34%2C11.2c.09%2C0%2C.14-.04.16-.13l.23-.94c.11-.03.21-.06.31-.1s.19-.08.26-.13l.82.51c.07.05.14.04.21-.03l.47-.47c.06-.06.07-.13.02-.21l-.51-.82c.04-.08.08-.17.12-.27s.07-.2.1-.3l.95-.22c.09-.02.13-.07.13-.16v-.67c0-.09-.04-.14-.13-.16l-.94-.22c-.03-.11-.06-.22-.11-.32s-.08-.18-.12-.26l.51-.82c.05-.08.04-.15-.02-.21l-.47-.47c-.07-.06-.14-.07-.22-.03l-.82.51c-.08-.04-.16-.08-.26-.12s-.2-.08-.31-.11l-.23-.95c-.02-.09-.07-.13-.16-.13h-.68c-.09%2C0-.14.04-.16.13l-.22.94c-.1.03-.21.06-.31.1s-.19.08-.27.12l-.82-.5c-.07-.05-.15-.04-.21.03l-.48.47c-.06.06-.07.13-.02.21l.51.82c-.03.07-.07.16-.11.26s-.08.2-.11.32l-.94.22c-.09.02-.13.07-.13.16v.67c0%2C.09.04.14.13.16l.95.22c.03.11.06.21.1.3s.08.19.12.27l-.51.82c-.05.08-.04.15.02.21l.47.47c.07.07.14.08.21.03l.82-.51c.08.04.17.09.27.13s.2.07.31.1l.22.94c.02.09.07.13.16.13h.68Z%22%2F%3E %3C%2Fg%3E %3Cg id%3D%22Layer_3%22%3E %3Crect class%3D%22cls-2%22 x%3D%225.46%22 y%3D%227.01%22 width%3D%221.07%22 height%3D%222.68%22 rx%3D%22.34%22 ry%3D%22.34%22%2F%3E %3Ccircle class%3D%22cls-2%22 cx%3D%226%22 cy%3D%226.04%22 r%3D%22.65%22%2F%3E %3C%2Fg%3E %3Cg id%3D%22Layer_2%22%3E %3Crect class%3D%22cls-1%22 x%3D%22.5%22 y%3D%22.5%22 width%3D%2211%22 height%3D%2214%22 rx%3D%221%22 ry%3D%221%22%2F%3E %3C%2Fg%3E%3C%2Fsvg%3E") no-repeat center / contain; -} - -.inspect-icon-home::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg id%3D%22Layer_2%22 xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 15.62 14.2%22%3E %3Cdefs%3E %3Cstyle%3E .cls-1 %7B fill%3A %23010101%3B %7D %3C%2Fstyle%3E %3C%2Fdefs%3E %3Crect class%3D%22cls-1%22 x%3D%2212.78%22 y%3D%225.56%22 width%3D%221.5%22 height%3D%228.48%22 rx%3D%22.75%22 ry%3D%22.75%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%221.78%22 y%3D%225.08%22 width%3D%221.5%22 height%3D%228.95%22 rx%3D%22.75%22 ry%3D%22.75%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%227.29%22 y%3D%227.33%22 width%3D%221.5%22 height%3D%2212.24%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(21.49 5.42) rotate(90)%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%223.53%22 y%3D%22-1.55%22 width%3D%221.5%22 height%3D%2211%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(4.35 -1.87) rotate(47.95)%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%2210.59%22 y%3D%22-1.57%22 width%3D%221.5%22 height%3D%2211%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(21.85 -1.86) rotate(132.05)%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%228.78%22 y%3D%227.74%22 width%3D%221%22 height%3D%225.47%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%226.28%22 y%3D%227.72%22 width%3D%221%22 height%3D%225.49%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%226.29%22 y%3D%227.72%22 width%3D%223.46%22 height%3D%221%22%2F%3E%3C%2Fsvg%3E") no-repeat center / contain; - -webkit-mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg id%3D%22Layer_2%22 xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 15.62 14.2%22%3E %3Cdefs%3E %3Cstyle%3E .cls-1 %7B fill%3A %23010101%3B %7D %3C%2Fstyle%3E %3C%2Fdefs%3E %3Crect class%3D%22cls-1%22 x%3D%2212.78%22 y%3D%225.56%22 width%3D%221.5%22 height%3D%228.48%22 rx%3D%22.75%22 ry%3D%22.75%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%221.78%22 y%3D%225.08%22 width%3D%221.5%22 height%3D%228.95%22 rx%3D%22.75%22 ry%3D%22.75%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%227.29%22 y%3D%227.33%22 width%3D%221.5%22 height%3D%2212.24%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(21.49 5.42) rotate(90)%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%223.53%22 y%3D%22-1.55%22 width%3D%221.5%22 height%3D%2211%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(4.35 -1.87) rotate(47.95)%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%2210.59%22 y%3D%22-1.57%22 width%3D%221.5%22 height%3D%2211%22 rx%3D%22.75%22 ry%3D%22.75%22 transform%3D%22translate(21.85 -1.86) rotate(132.05)%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%228.78%22 y%3D%227.74%22 width%3D%221%22 height%3D%225.47%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%226.28%22 y%3D%227.72%22 width%3D%221%22 height%3D%225.49%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%226.29%22 y%3D%227.72%22 width%3D%223.46%22 height%3D%221%22%2F%3E%3C%2Fsvg%3E") no-repeat center / contain; -} - -.inspect-icon-tasks::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - margin-top: -2px; - mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 16 16%22%3E %3Cdefs%3E %3Cstyle%3E .cls-1 %7B fill%3A %23231f20%3B %7D %3C%2Fstyle%3E %3C%2Fdefs%3E %3Cg id%3D%22Layer_5%22 data-name%3D%22Layer 5%22%3E %3Crect class%3D%22cls-1%22 x%3D%227.07%22 y%3D%222.57%22 width%3D%227.02%22 height%3D%221%22 rx%3D%22.5%22 ry%3D%22.5%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%227.07%22 y%3D%227.57%22 width%3D%227.02%22 height%3D%221%22 rx%3D%22.5%22 ry%3D%22.5%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%227.07%22 y%3D%2212.57%22 width%3D%227.02%22 height%3D%221%22 rx%3D%22.5%22 ry%3D%22.5%22%2F%3E %3C%2Fg%3E %3Cg id%3D%22Layer_4%22 data-name%3D%22Layer 4%22%3E %3Cpath class%3D%22cls-1%22 d%3D%22M3.54%2C4.87c-.09%2C0-.17-.02-.24-.06-.07-.04-.14-.1-.21-.19l-1.05-1.34c-.08-.11-.12-.22-.12-.33%2C0-.12.04-.22.12-.3.08-.08.18-.13.29-.13.08%2C0%2C.14.02.21.05.06.03.13.09.19.17l.8%2C1.1%2C1.23-2.58c.1-.16.22-.24.38-.24.11%2C0%2C.21.04.3.11.09.07.13.17.13.28%2C0%2C.06-.01.12-.04.18-.03.06-.06.11-.09.16l-1.46%2C2.87c-.11.17-.25.25-.43.25Z%22%2F%3E %3Cpath class%3D%22cls-1%22 d%3D%22M3.54%2C9.87c-.09%2C0-.17-.02-.24-.06-.07-.04-.14-.1-.21-.19l-1.05-1.34c-.08-.11-.12-.22-.12-.33%2C0-.12.04-.22.12-.3.08-.08.18-.13.29-.13.08%2C0%2C.14.02.21.05.06.03.13.09.19.17l.8%2C1.1%2C1.23-2.58c.1-.16.22-.24.38-.24.11%2C0%2C.21.04.3.11.09.07.13.17.13.28%2C0%2C.06-.01.12-.04.18-.03.06-.06.11-.09.16l-1.46%2C2.87c-.11.17-.25.25-.43.25Z%22%2F%3E %3Cpath class%3D%22cls-1%22 d%3D%22M3.54%2C14.87c-.09%2C0-.17-.02-.24-.06-.07-.04-.14-.1-.21-.19l-1.05-1.34c-.08-.11-.12-.22-.12-.33%2C0-.12.04-.22.12-.3.08-.08.18-.13.29-.13.08%2C0%2C.14.02.21.05.06.03.13.09.19.17l.8%2C1.1%2C1.23-2.58c.1-.16.22-.24.38-.24.11%2C0%2C.21.04.3.11.09.07.13.17.13.28%2C0%2C.06-.01.12-.04.18-.03.06-.06.11-.09.16l-1.46%2C2.87c-.11.17-.25.25-.43.25Z%22%2F%3E %3C%2Fg%3E%3C%2Fsvg%3E") no-repeat center / contain; - -webkit-mask: url("data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22UTF-8%22%3F%3E%3Csvg xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 16 16%22%3E %3Cdefs%3E %3Cstyle%3E .cls-1 %7B fill%3A %23231f20%3B %7D %3C%2Fstyle%3E %3C%2Fdefs%3E %3Cg id%3D%22Layer_5%22 data-name%3D%22Layer 5%22%3E %3Crect class%3D%22cls-1%22 x%3D%227.07%22 y%3D%222.57%22 width%3D%227.02%22 height%3D%221%22 rx%3D%22.5%22 ry%3D%22.5%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%227.07%22 y%3D%227.57%22 width%3D%227.02%22 height%3D%221%22 rx%3D%22.5%22 ry%3D%22.5%22%2F%3E %3Crect class%3D%22cls-1%22 x%3D%227.07%22 y%3D%2212.57%22 width%3D%227.02%22 height%3D%221%22 rx%3D%22.5%22 ry%3D%22.5%22%2F%3E %3C%2Fg%3E %3Cg id%3D%22Layer_4%22 data-name%3D%22Layer 4%22%3E %3Cpath class%3D%22cls-1%22 d%3D%22M3.54%2C4.87c-.09%2C0-.17-.02-.24-.06-.07-.04-.14-.1-.21-.19l-1.05-1.34c-.08-.11-.12-.22-.12-.33%2C0-.12.04-.22.12-.3.08-.08.18-.13.29-.13.08%2C0%2C.14.02.21.05.06.03.13.09.19.17l.8%2C1.1%2C1.23-2.58c.1-.16.22-.24.38-.24.11%2C0%2C.21.04.3.11.09.07.13.17.13.28%2C0%2C.06-.01.12-.04.18-.03.06-.06.11-.09.16l-1.46%2C2.87c-.11.17-.25.25-.43.25Z%22%2F%3E %3Cpath class%3D%22cls-1%22 d%3D%22M3.54%2C9.87c-.09%2C0-.17-.02-.24-.06-.07-.04-.14-.1-.21-.19l-1.05-1.34c-.08-.11-.12-.22-.12-.33%2C0-.12.04-.22.12-.3.08-.08.18-.13.29-.13.08%2C0%2C.14.02.21.05.06.03.13.09.19.17l.8%2C1.1%2C1.23-2.58c.1-.16.22-.24.38-.24.11%2C0%2C.21.04.3.11.09.07.13.17.13.28%2C0%2C.06-.01.12-.04.18-.03.06-.06.11-.09.16l-1.46%2C2.87c-.11.17-.25.25-.43.25Z%22%2F%3E %3Cpath class%3D%22cls-1%22 d%3D%22M3.54%2C14.87c-.09%2C0-.17-.02-.24-.06-.07-.04-.14-.1-.21-.19l-1.05-1.34c-.08-.11-.12-.22-.12-.33%2C0-.12.04-.22.12-.3.08-.08.18-.13.29-.13.08%2C0%2C.14.02.21.05.06.03.13.09.19.17l.8%2C1.1%2C1.23-2.58c.1-.16.22-.24.38-.24.11%2C0%2C.21.04.3.11.09.07.13.17.13.28%2C0%2C.06-.01.12-.04.18-.03.06-.06.11-.09.16l-1.46%2C2.87c-.11.17-.25.25-.43.25Z%22%2F%3E %3C%2Fg%3E%3C%2Fsvg%3E") no-repeat center / contain; -} - -/* Use this very carefully, find relies upon checking the -document selection to determine if it should skip a find match -so disabling selection can break that functionality */ -.hideSelection { - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; -} -.error-panel { - flex-direction: column; - min-height: 10rem; - margin-top: 4rem; - margin-bottom: 4em; - width: 100%; -} -.error-panel-heading { - font-size: var(--inspect-font-size-larger); -} - -.error-panel-body { - display: inline-block; - font-size: var(--inspect-font-size-smaller); - margin-top: 1rem; - border: solid 1px var(--bs-border-color); - border-radius: var(--bs-border-radius); - padding: 1em; - max-width: 80%; -} - -.error-panel-stack { - font-size: var(--inspect-font-size-smaller); - white-space: prewrap; -} - -.centered-flex { - display: flex; - flex: 0 0 content; - align-items: center; - justify-content: start; -} - -.error-icon { - margin-right: 0.5rem; - color: var(--bs-red); -} -._wrapper_xqbef_1 { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 0; - overflow-y: visible; - overflow-x: visible; - text-align: center; -} - -._container_xqbef_12 { - width: 100%; - height: 0px; - background-color: transparent; - position: sticky; - overflow: visible; - z-index: 1200; -} - -._animate_xqbef_21 { - width: 5%; - height: 1px; - animation: _leftToRight_xqbef_1 2s linear infinite; - background-color: #3b82f6; - position: absolute; - left: 0; - top: 0; -} - -@keyframes _leftToRight_xqbef_1 { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(2000%); - } -} -._panel_twp3v_1 { - width: 100%; - display: flex; - justify-content: center; -} - -._container_twp3v_7 { - margin-top: 3em; - display: grid; - grid-template-columns: max-content max-content; - column-gap: 0.3em; -} -._container_1g7rb_1 { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; -} - -._defineScannerSection_1g7rb_8 { - padding: 1rem; - border-bottom: 1px solid var(--bs-border-color); - flex-shrink: 0; -} - -._sectionTitle_1g7rb_14 { - font-size: 1rem; - font-weight: 600; - margin: 0 0 1rem 0; -} - -._formRow_1g7rb_20 { - display: flex; - gap: 2rem; -} - -._formColumn_1g7rb_25 { - display: flex; - flex-direction: column; - gap: 0.75rem; - flex: 1; -} - -._formGroup_1g7rb_32 { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -._scannerRow_1g7rb_38 { - display: flex; - align-items: center; - gap: 0.75rem; -} - -._scannerDescription_1g7rb_44 { - flex: 2; - font-size: 0.75rem; - color: var(--bs-secondary); -} - -._paramsPlaceholder_1g7rb_50 { - padding: 1rem; - color: var(--bs-secondary); - font-size: 0.875rem; -} - -._runScanRow_1g7rb_56 { - display: flex; - align-items: center; - gap: 1rem; - margin-top: 0.75rem; -} - -._checkboxGroup_1g7rb_63 { - display: flex; - gap: 1.5rem; -} - -._scansList_1g7rb_68 { - display: flex; - flex-direction: column; - flex: 1; - padding: 1rem; - gap: 1rem; - overflow: auto; -} - -._card_1g7rb_77 { - border: 1px solid var(--bs-border-color); - border-radius: 0.375rem; - padding: 1rem; - background-color: var(--bs-body-bg); -} - -._header_1g7rb_84 { - margin-bottom: 0.75rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid var(--bs-border-color); -} - -._scanId_1g7rb_90 { - font-weight: 600; - font-family: monospace; -} - -._configLine_1g7rb_95 { - color: var(--bs-secondary); - font-size: 0.75rem; - font-family: monospace; - margin-top: 0.25rem; -} - -._progressSection_1g7rb_102 { - margin-bottom: 0.75rem; -} - -._progressBar_1g7rb_106 { - height: 4px; - background-color: var(--bs-secondary-bg); - border-radius: 2px; - overflow: hidden; -} - -._progressFill_1g7rb_113 { - height: 100%; - background-color: var(--bs-success); - transition: width 0.3s ease; -} - -._progressStats_1g7rb_119 { - display: flex; - gap: 1.5rem; - margin-top: 0.25rem; - font-family: monospace; - font-size: 0.75rem; - color: var(--bs-secondary); -} - -._content_1g7rb_128 { - display: flex; - gap: 2rem; -} - -._mainSection_1g7rb_133 { - flex: 1; -} - -._table_1g7rb_137 { - width: 100%; - border-collapse: collapse; - font-family: monospace; - font-size: 0.875rem; -} - -._table_1g7rb_137 th, -._table_1g7rb_137 td { - padding: 0.25rem 0.75rem; - text-align: left; -} - -._table_1g7rb_137 th { - font-weight: 600; - color: var(--bs-secondary); -} - -._numeric_1g7rb_155 { - text-align: right; -} - -._sidebar_1g7rb_159 { - min-width: 150px; - border-left: 1px solid var(--bs-border-color); - padding-left: 1rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -._sidebarSection_1g7rb_168 { - font-family: monospace; - font-size: 0.875rem; -} - -._sidebarTitle_1g7rb_173 { - font-weight: 600; - margin-bottom: 0.5rem; -} - -._stat_1g7rb_178 { - display: flex; - justify-content: space-between; - gap: 1rem; -} - -._stat_1g7rb_178 span:first-child { - color: var(--bs-secondary); -} - -._error_1g7rb_188 { - color: var(--bs-danger); - font-weight: 600; -} - -._mutationStatus_1g7rb_193 { - font-size: 0.7rem; - color: var(--bs-secondary); -} -._container_at9ra_1 { - display: grid; - grid-template-columns: max-content max-content minmax(0, max-content); - align-items: center; - border: solid 1px var(--bs-border-color); - border-radius: var(--bs-border-radius); - width: 100%; -} - -._labelContainer_at9ra_10 { - border-right: solid 1px var(--bs-border-color); - padding: 0 0.45rem; - height: 100%; - display: flex; - align-items: center; -} - -._labelAdornmentIcon_at9ra_18 { - margin-left: 0.5rem; -} - -._icon_at9ra_22 { - color: var(--bs-body-color); - opacity: 0.7; - margin-right: 0.35rem; -} - -._text_at9ra_28 { - color: var(--bs-body-color); - white-space: nowrap; - cursor: text; - padding: 0.125rem 0.25rem; - border-top-right-radius: var(--bs-border-radius); - border-bottom-right-radius: var(--bs-border-radius); - transition: background-color 0.2s; - min-width: 2ch; - display: inline-block; - background-color: var(--bs-body-bg); -} - -._text_at9ra_28._readOnly_at9ra_41 { - background-color: var(--bs-light); -} - -._text_at9ra_28:hover { - background-color: var(--bs-secondary-bg); -} - -._text_at9ra_28:focus { - outline: 1px solid var(--bs-primary); - outline-offset: 0; - background-color: var(--bs-body-bg); -} - -._text_at9ra_28._placeholder_at9ra_55 { - color: var(--bs-secondary-color); - opacity: 0.6; -} - -._text_at9ra_28._secondary_at9ra_60 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -._mruPopover_at9ra_66 { - padding: 0 !important; - max-height: 300px; - overflow-y: auto; - width: 80%; -} - -._mruList_at9ra_73 { - display: flex; - flex-direction: column; - min-width: 150px; -} - -._mruItem_at9ra_79 { - padding: 0.3rem 0.5rem; - cursor: pointer; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - transition: background-color 0.15s; - color: var(--bs-body-color); -} - -._mruItem_at9ra_79:hover, -._mruItemSelected_at9ra_90 { - background-color: var(--bs-secondary-bg); -} - -._mruItem_at9ra_79:active { - background-color: var(--bs-tertiary-bg); -} - -._mruItem_at9ra_79:first-child { - border-top-left-radius: var(--bs-border-radius); - border-top-right-radius: var(--bs-border-radius); -} - -._mruItem_at9ra_79:last-child { - border-bottom-left-radius: var(--bs-border-radius); - border-bottom-right-radius: var(--bs-border-radius); -} -._header_1q9f5_1 { - background: var(--bs-light); - display: grid; - grid-template-columns: max-content 1fr max-content max-content; - justify-content: space-between; - padding: 0.2em 0.5em; - overflow: hidden; -} - -._bordered_1q9f5_10 { - border-bottom: solid var(--bs-border-color) 1px; -} - -._left_1q9f5_14 { - overflow: hidden; - min-width: 0; - max-width: 100%; -} - -._leftButtons_1q9f5_20 { - display: flex; - gap: 0.4rem; - align-items: baseline; - padding-right: 0.4rem; - margin-right: 0.4rem; - border-right: solid var(--bs-border-color) 1px; -} - -._rightButtons_1q9f5_29 { - display: flex; - gap: 0.4rem; - align-items: center; - padding-left: 0.4rem; - margin-left: 0.4rem; - border-left: solid var(--bs-border-color) 1px; -} - -._hasChildren_1q9f5_38 { - border-left: solid var(--bs-border-color) 1px; - padding-left: 0.5rem; - margin-left: 0.5rem; -} -._toolbarButton_1ldhr_1 { - color: var(--bs-body-color); -} - -._toolbarButton_1ldhr_1:hover { - color: var(--bs-link-hover-color); -} - -._toolbarButton_1ldhr_1._disabled_1ldhr_9 { - color: var(--bs-secondary); -} - -._toolbarButton_1ldhr_1._disabled_1ldhr_9:hover { - color: var(--bs-secondary); - cursor: default; -} -._filterButton_fuk0s_1 { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 2px; - border: none; - background: transparent; - color: var(--bs-secondary); - cursor: pointer; -} - -._filterButtonActive_fuk0s_12 { - color: var(--bs-primary); -} -._headerActions_uh0qk_1 { - display: flex; - align-items: center; - gap: 4px; - margin-right: 6px; -} - -._filterPopover_uh0qk_8 { - min-width: 180px; -} -._container_1qaxn_1 { - position: relative; - width: 100%; -} - -._input_1qaxn_6 { - width: 100%; - font-size: 12px; - padding: 4px 6px; - border-radius: 4px; - border: 1px solid var(--bs-border-color); - background: var(--bs-body-bg); - color: var(--bs-body-color); -} - -._input_1qaxn_6:disabled { - background: var(--bs-light); - color: var(--bs-secondary); -} - -._inputWithToggle_1qaxn_21 { - padding-right: 24px; -} - -._toggleButton_1qaxn_25 { - position: absolute; - right: 2px; - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - padding: 2px 4px; - cursor: pointer; - color: var(--bs-body-color); - display: flex; - align-items: center; - justify-content: center; - font-size: 10px; -} - -._toggleButton_1qaxn_25:hover:not(:disabled) { - color: var(--bs-body-color); -} - -._toggleButton_1qaxn_25:disabled { - cursor: not-allowed; - opacity: 0.5; -} - -._suggestionsList_1qaxn_50 { - margin: 0; - padding: 0; - list-style: none; - background: var(--bs-body-bg); - border: 1px solid var(--bs-border-color); - border-radius: 4px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - max-height: 200px; - overflow-y: auto; - z-index: 10000; -} - -._suggestionItem_1qaxn_63 { - padding: 6px 8px; - font-size: 12px; - cursor: pointer; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -._suggestionItem_1qaxn_63:hover, -._suggestionItem_1qaxn_63._highlighted_1qaxn_73 { - background: var(--bs-primary); - color: white; -} -._filterContent_1e9kw_1 { - display: flex; - flex-direction: column; - gap: 8px; -} - -._filterRow_1e9kw_7 { - display: flex; - align-items: center; - gap: 8px; -} - -._filterSelect_1e9kw_13, -._filterInput_1e9kw_14 { - flex: 1; - font-size: 12px; - padding: 4px 6px; - border-radius: 4px; - border: 1px solid var(--bs-border-color); - background: var(--bs-body-bg); - color: var(--bs-body-color); -} - -._filterInput_1e9kw_14:disabled, -._filterSelect_1e9kw_13:disabled { - background: var(--bs-light); - color: var(--bs-secondary); -} - -._columnId_1e9kw_30 { - font-weight: 600; - font-family: var(--bs-monospace); -} - -._columnIdText_1e9kw_35 { - margin-left: 0.5rem; -} - -.btn._filterButton_1e9kw_39 { - width: 100%; - padding: 0.2rem; -} - -._durationInputWrapper_1e9kw_44 { - display: flex; - flex-direction: column; - gap: 4px; - flex: 1; -} - -._durationHelper_1e9kw_51 { - font-size: 11px; - color: var(--bs-secondary); - padding-left: 2px; -} - -._rangeLabel_1e9kw_57 { - font-size: 11px; - color: var(--bs-secondary); - padding-left: 2px; -} -._dropdownContainer_1fize_1 { - position: relative; - display: inline-flex; - align-items: center; -} - -.btn._toolButton_1fize_7 { - background-color: var(--bs-light-border-subtle); -} - -.btn._toolButton_1fize_7._bodyColor_1fize_11 { - background-color: var(--bs-body-bg); -} - -.btn-tools._toolButton_1fize_7 { - border: solid 1px var(--bs-border-color); -} - -._toolButton_1fize_7 { - white-space: nowrap; -} - -._toolButton_1fize_7 i { - margin-right: 0.5em; -} - -._toolButton_1fize_7 ._chevron_1fize_27 { - margin-left: 0.5em; - margin-right: 0; - font-size: 0.75em; -} - -._toolButton_1fize_7:focus { - outline: none; - box-shadow: - 0 0 0 2px rgba(var(--bs-primary-rgb), 0.5), - inset 0 2px 4px rgba(0, 0, 0, 0.15), - inset 0 1px 2px rgba(0, 0, 0, 0.1); -} - -._backdrop_1fize_41 { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 998; -} - -._dropdownMenu_1fize_50 { - position: absolute; - top: 100%; - left: 0; - margin-top: 0.25rem; - background: var(--bs-body-bg); - border: 1px solid var(--bs-border-color); - border-radius: 0.25rem; - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); - z-index: 999; - min-width: 100%; - overflow: hidden; -} - -._dropdownMenu_1fize_50._alignRight_1fize_64 { - right: 0; - left: auto; -} - -._dropdownItem_1fize_69 { - display: block; - width: 100%; - padding: 0.25rem 0.75rem; - background: none; - border: none; - text-align: left; - cursor: pointer; - color: var(--bs-body-color); - white-space: nowrap; - transition: background-color 0.15s ease-in-out; -} - -._dropdownItem_1fize_69:hover { - background-color: var(--bs-tertiary-bg); -} - -._dropdownItem_1fize_69:active { - background-color: var(--bs-secondary-bg); -} -._chip_4n1t8_1 { - display: inline-flex; - flex-direction: row; - margin: 0rem 0; - padding: 2px 6px; - border: solid 1px var(--bs-border-color); - border-radius: var(--bs-border-radius); - align-items: baseline; - white-space: nowrap; -} - -._label_4n1t8_12 { - font-weight: 600; - margin-right: 0.5em; -} - -._icon_4n1t8_17 { - margin-right: 0.2rem; - font-size: 0.9em; -} - -._closeIcon_4n1t8_22 { - margin-left: 0.3rem; - font-size: 0.9em; -} - -._clickable_4n1t8_27 { - cursor: pointer; -} -._container_1ljjt_1 { - display: flex; - flex-direction: row; - background: var(--bs-light-bg-subtle); - border-bottom: solid var(--bs-border-color) 1px; - padding: 0rem 0.5rem; - align-items: flex-start; - width: 100%; -} - -._filterBar_1ljjt_11 { - margin: 1px 0; -} - -._filterLabel_1ljjt_15 { - margin-right: 0.3rem; - margin-top: 3px; -} - -._filterNone_1ljjt_20 { - margin-top: 2px; -} - -._actionButtons_1ljjt_24 { - margin-left: auto; - margin-top: 0.1rem; - margin-bottom: 0.1rem; - display: flex; - justify-content: flex-end; - align-items: flex-start; - flex-direction: row; - font-size: var(--inspect-font-size-smallest); - height: calc(100% - 4px); -} - -._actionButton_1ljjt_24 { - padding: 0.1rem 0.4rem; - margin: 0; - font-size: var(--inspect-font-size-smallest); -} - -._actionButtonDropDown_1ljjt_42 { - font-size: var(--inspect-font-size-smallest); -} - -._actionButton_1ljjt_24._chipButton_1ljjt_46 { - background-color: unset; - margin-left: 0.3rem; -} - -._sep_1ljjt_51 { - width: 1px; - height: calc(100% - 2px); - margin: 2px 0.3rem; - - background-color: var(--bs-border-color); - font-size: var(--inspect-font-size-smallestest); -} - -._filterChip_1ljjt_60 { - /* Filter chip specific styles can go here */ -} - -._columnsButton_1ljjt_64 { - padding: 0.1rem 0.3rem; -} -._chipGroup_1de1s_1 { - display: flex; - flex-direction: row; - flex-wrap: wrap; - column-gap: 0.3rem; - row-gap: 0.3rem; -} -.btn._toolButton_wcmr6_1 { - background-color: var(--bs-light-border-subtle); - white-space: nowrap; -} - -._marginRight_wcmr6_6 { - margin-right: 0.5em; -} - -._toolButton_wcmr6_1:focus { - outline: none; - box-shadow: - 0 0 0 2px rgba(var(--bs-primary-rgb), 0.5), - inset 0 2px 4px rgba(0, 0, 0, 0.15), - inset 0 1px 2px rgba(0, 0, 0, 0.1); -} - -._latched_wcmr6_18, -._latched_wcmr6_18:hover { - box-shadow: - inset 0 2px 4px rgba(0, 0, 0, 0.15), - inset 0 1px 2px rgba(0, 0, 0, 0.1); -} - -.btn._toolButton_wcmr6_1._subtle_wcmr6_25 { - background-color: var(--bs-body-bg); -} -._columnList_1wz3s_1 { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.15em 1em; - max-height: 400px; - overflow-y: auto; -} - -._row_1wz3s_9 { - display: flex; - align-items: center; - gap: 0.5em; - cursor: pointer; - border-radius: var(--bs-border-radius); - padding: 0.1em 0.4em; - margin: 0 -0.4em; -} - -._row_1wz3s_9:hover { - background-color: var(--bs-secondary-bg); -} - -._links_1wz3s_23 { - display: flex; - padding-bottom: 0.2em; - margin-bottom: 0.4em; - column-gap: 0.3em; - border-bottom: solid 1px var(--bs-border-color); -} - -._links_1wz3s_23 a { - cursor: pointer; - text-decoration: none; - color: var(--bs-link-color); -} - -._links_1wz3s_23 a:hover { - color: var(--bs-link-hover-color); -} - -._selected_1wz3s_41 { - font-weight: 600; -} -._success_1mf4d_1 { - color: var(--bs-success) !important; -} - -._unsuccess_1mf4d_5 { - opacity: 0.5 !important; -} -._footer_1ykeg_1 { - border-top: solid var(--bs-light-border-subtle) 1px; - background: var(--bs-light-bg-subtle); - display: grid; - grid-template-columns: max-content 1fr max-content; - justify-content: space-between; - - padding: 0.2em 1em; -} - -._spinnerContainer_1ykeg_11 { - display: grid; - grid-template-columns: max-content max-content; - column-gap: 0.3em; - padding-top: 0.2em; -} - -._spinner_1ykeg_11 { - height: 11px !important; - width: 11px !important; - color: var(--bs-secondary) !important; - border-width: 1px !important; -} - -._label_1ykeg_25 { - margin-left: 0.1em; - margin-top: -3px; -} - -._right_1ykeg_30 { - display: grid; - grid-auto-columns: max-content; - grid-auto-flow: column; - justify-content: end; - align-items: center; - column-gap: 0.5em; -} - -._left_1ykeg_39 { - display: grid; - grid-auto-columns: max-content; - grid-auto-flow: column; - justify-content: start; - align-items: center; - column-gap: 0.5em; -} - -._center_1ykeg_48 { - display: grid; - grid-auto-columns: max-content; - grid-auto-flow: column; - justify-content: center; - align-items: center; - column-gap: 0.5em; -} -._pager_jzegk_1 { - --bs-pagination-padding-x: 0.5em; - --bs-pagination-padding-y: 0.15em; - --bs-pagination-font-size: 0.8rem; - --bs-pagination-padding-x: 0.5em; - --bs-pagination-padding-y: 0; - --bs-pagination-border-radius: var(--bs-border-radius); - margin-bottom: 0; -} - -._item_jzegk_11:not(.disabled) { - cursor: pointer; -} - -._item_jzegk_11 { - margin-left: 0.2em; - margin-right: 0.2em; -} - -._item_jzegk_11:first-child, -._item_jzegk_11:last-child { - --bs-pagination-padding-x: 0.3em; -} - -._item_jzegk_11:first-child .ii:before, -._item_jzegk_11:last-child .ii:before { - margin-top: -2px; - font-size: 0.8em; -} -._green_ynv0j_1 { - color: var(--bs-success); -} - -._yellow_ynv0j_5 { - color: var(--bs-warning); -} - -._red_ynv0j_9 { - color: var(--bs-danger); -} - -._blue_ynv0j_13 { - color: var(--bs-primary); -} -._container_8dclc_1 { - overflow: auto; - outline: none; -} - -._table_8dclc_6 { - display: grid; - width: 100%; -} - -._thead_8dclc_11 { - display: grid; - position: sticky; - top: 0; - z-index: 1; - background: var(--bs-light-bg-subtle); - border-bottom: 1px solid var(--bs-border-color); -} - -._headerRow_8dclc_20 { - display: flex; - width: 100%; - height: 32px; -} - -._headerCell_8dclc_26 { - display: flex; - position: relative; - align-items: center; - justify-content: space-between; - gap: 4px; - padding: 4px 0px 4px 8px; - text-align: left; - font-weight: 400; - color: var(--bs-secondary); - font-size: var(--inspect-font-size-small) !important; - transition: background-color 0.15s ease; -} - -._headerContent_8dclc_40 { - flex: 1; - display: flex; - align-items: center; - gap: 4px; - cursor: grab; - user-select: none; -} - -._headerContent_8dclc_40:active { - cursor: grabbing; -} - -._headerText_8dclc_53 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -._sortIcon_8dclc_59 { - font-size: 10px; - margin-left: 2px; -} - -._headerCellDragging_8dclc_64 { - opacity: 1; - background: var(--bs-light-bg-subtle); -} - -._headerCellDragOverLeft_8dclc_69 { - border-left: 3px solid var(--bs-primary); -} - -._headerCellDragOverRight_8dclc_73 { - border-right: 3px solid var(--bs-primary); -} - -._resizer_8dclc_77 { - position: absolute; - right: 0; - top: 0; - margin-top: 4px; - margin-bottom: 4px; - height: calc(100% - 8px); - width: 5px; - background: transparent; - cursor: col-resize; - user-select: none; - touch-action: none; - border-right: solid 1px var(--bs-border-color); - z-index: 10; - pointer-events: auto; -} - -._headerCellDragging_8dclc_64 ._resizer_8dclc_77 { - border-right: none; -} - -._resizer_8dclc_77:hover { - background: var(--bs-primary); - opacity: 0.5; -} - -._resizerActive_8dclc_103 { - background: var(--bs-primary); - opacity: 1; -} - -._headerCell_8dclc_26:last-child { - border-right: none; -} - -._tbody_8dclc_112 { - display: grid; - position: relative; -} - -._row_8dclc_117 { - display: flex; - position: absolute; - width: 100%; - border-bottom: 1px solid var(--bs-border-color); - background: var(--bs-body-bg); - transition: background-color 0.15s ease; - cursor: pointer; - user-select: none; -} - -._row_8dclc_117:hover { - background: var(--bs-light); -} - -._rowSelected_8dclc_132 { - background: var(--bs-primary-bg-subtle) !important; -} - -._rowFocused_8dclc_136 { - outline: 2px solid var(--bs-primary-bg-subtle); - outline-offset: 0px; -} - -._rowSelected_8dclc_132._rowFocused_8dclc_136 { - background: var(--bs-primary-bg-subtle) !important; -} - -._cell_8dclc_145 { - display: flex; - align-items: center; - padding: 4px 8px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 12px !important; -} - -._cell_8dclc_145:last-child { - border-right: none; -} - -._cellCenter_8dclc_159 { - justify-content: center; -} - -._headerCellCenter_8dclc_163 { - justify-content: center; -} - -._noMatching_8dclc_167 { - grid-column: 1 / -1; - margin-left: auto; - margin-right: auto; - margin-top: 3rem; -} -._container_1a52k_1 { - display: grid; - grid-template-rows: max-content max-content 1fr max-content; - height: 100%; - row-gap: 0; -} - -._gridContainer_1a52k_8 { - display: flex; - flex-direction: column; - min-height: 0; - overflow: hidden; -} - -._grid_1a52k_8 { - flex: 1; - min-height: 0; - overflow: auto; -} -._container_w24zj_1 { - height: 100%; - display: grid; - grid-template-rows: max-content max-content max-content 1fr max-content; -} - -._panel_w24zj_7 { - display: grid; - grid-template-columns: max-content 1fr; -} -._backdrop_wrdr6_1 { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.36); - display: flex; - align-items: flex-start; - justify-content: center; - padding-top: 15vh; - z-index: 1000; -} - -._modal_wrdr6_12 { - background: var(--vscode-editor-background); - border: 1px solid - var(--vscode-widget-border, var(--vscode-editorWidget-border)); - border-radius: 4px; - box-shadow: 0 4px 8px var(--vscode-widget-shadow, rgba(0, 0, 0, 0.36)); - max-width: 450px; - width: 90%; - max-height: 80vh; - display: flex; - flex-direction: column; -} - -._header_wrdr6_25 { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - border-bottom: 1px solid - var(--vscode-widget-border, var(--vscode-editorWidget-border)); -} - -._title_wrdr6_34 { - margin: 0; - font-size: 14px; - font-weight: 600; - color: var(--vscode-foreground); -} - -._closeButton_wrdr6_41 { - background: none; - border: none; - color: var(--vscode-foreground); - cursor: pointer; - padding: 4px; - display: flex; - align-items: center; - justify-content: center; -} - -._closeButton_wrdr6_41:hover { - background: var(--vscode-toolbar-hoverBackground); - border-radius: 4px; -} - -._body_wrdr6_57 { - padding: 16px; - color: var(--vscode-foreground); - font-size: 13px; - overflow-y: auto; -} - -._footer_wrdr6_64 { - display: flex; - justify-content: flex-end; - gap: 8px; - padding: 12px 16px; - border-top: 1px solid - var(--vscode-widget-border, var(--vscode-editorWidget-border)); -} -._container_cr35j_1 { - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; - height: 100%; - width: 100%; - gap: 12px; - padding: 24px; -} - -._container_cr35j_1::before { - content: ""; - flex: 0 0 15%; -} - -._icon_cr35j_17 { - font-size: 48px; - color: var(--vscode-descriptionForeground); - opacity: 0.7; -} - -._title_cr35j_23 { - font-size: 16px; - font-weight: 600; - color: var(--vscode-foreground); - text-align: center; -} - -._description_cr35j_30 { - font-size: 14px; - color: var(--vscode-descriptionForeground); - text-align: center; - max-width: 300px; -} - -._action_cr35j_37 { - margin-top: 4px; -} - -._action_cr35j_37 a { - color: var(--bs-link-color); - text-decoration: none; - font-size: 14px; -} - -._action_cr35j_37 a:hover { - color: var(--bs-link-hover-color); - text-decoration: underline; -} -._container_1linb_1 { - display: grid; - grid-template-columns: 1fr max-content; - column-gap: 0.3em; - border: solid var(--bs-border-color) 1px; - border-radius: var(--bs-border-radius); - background-color: var(--inspect-input-background); - align-items: center; - overflow: hidden; -} - -._container_1linb_1:focus-within { - outline: 2px solid var(--bs-primary); - outline-offset: 2px; -} - -._input_1linb_17 { - border: none; - outline: none; - background: transparent; - color: var(--bs-body-color); - min-width: 0; -} - -._withIcon_1linb_25 { - grid-template-columns: max-content 1fr max-content; -} - -._icon_1linb_29 { - margin-left: 0.3em; -} - -._clearText_1linb_33 { - opacity: 0.6; - margin-right: 0.3em; -} - -._clearText_1linb_33:hover { - cursor: pointer; -} - -._clearText_1linb_33._hidden_1linb_42:hover { - cursor: default !important; -} -._hidden_1linb_42 { - opacity: 0 !important; -} -._modalContent_x5gpr_1 { - display: flex; - flex-direction: column; - gap: 16px; -} - -._modalContent_x5gpr_1 p { - margin: 0; - color: var(--vscode-foreground); -} - -._fieldGroup_x5gpr_12 { - display: flex; - flex-direction: column; - gap: 6px; -} - -._label_x5gpr_18 { - font-size: 0.85rem; - color: var(--vscode-descriptionForeground); -} - -._select_x5gpr_23 { - width: 100%; -} - -._textInput_x5gpr_27 { - width: 100%; -} - -._hint_x5gpr_31 { - font-size: 0.8rem; - color: var(--vscode-descriptionForeground); - font-style: italic; -} - -._error_x5gpr_37 { - color: var(--vscode-errorForeground); - font-size: 0.9em; - margin: 0; -} -._card_1lurf_1 { - display: grid; - /* grid-template-columns set dynamically via inline style */ - align-items: center; - gap: 12px; - padding: calc(0.3rem + 1px) 1rem; - background: transparent; - border: none; - border-radius: 0; - border-bottom: solid 1px var(--bs-border-color); - cursor: pointer; -} - -._card_1lurf_1:hover { - background: var( - --vscode-list-inactiveSelectionBackground, - rgba(128, 128, 128, 0.04) - ); -} - -._card_1lurf_1._selected_1lurf_21 { - /* No special background for selected rows */ -} - -._checkbox_1lurf_25 { - display: flex; - align-items: center; - justify-content: center; - transform: scale(0.85); -} - -._transcriptCell_1lurf_32 { - display: flex; - flex-direction: column; - min-width: 0; - overflow: hidden; -} - -._idLink_1lurf_39 { - background: none; - border: none; - padding: 0; - color: #5a9bcf; - cursor: pointer; - font-size: 0.85rem; - text-decoration: none; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-align: left; -} - -._idLink_1lurf_39:hover:not(:disabled) { - text-decoration: underline; -} - -._idLink_1lurf_39:disabled { - color: var(--vscode-disabledForeground); - cursor: default; -} - -._labelsCell_1lurf_62 { - text-align: right; - min-width: 80px; - padding-right: 8px; - font-size: 0.8rem; - color: var(--vscode-descriptionForeground); -} - -._targetCell_1lurf_70 { - text-align: right; - min-width: 80px; - padding-right: 8px; -} - -._target_1lurf_70 { - color: var(--vscode-descriptionForeground); - font-size: 0.85rem; -} - -/* Boolean target badges */ -._targetTrue_1lurf_82 { - display: inline-flex; - justify-content: center; - align-items: center; - border-radius: 5px; - padding: 4px 8px; - font-size: 0.7rem; - background-color: var(--bs-success); - border: solid var(--bs-success) 1px; - color: var(--bs-body-bg); - width: 4.5em; -} - -._targetFalse_1lurf_95 { - display: inline-flex; - justify-content: center; - align-items: center; - border-radius: 5px; - padding: 4px 8px; - font-size: 0.7rem; - background-color: var(--bs-danger); - border: solid var(--bs-danger) 1px; - color: var(--bs-body-bg); - width: 4.5em; -} - -/* Subtle split selector */ -._splitSelect_1lurf_109 { - width: 90px !important; - min-width: 90px !important; - max-width: 90px !important; - --vscode-dropdown-border: transparent; - --vscode-dropdown-background: transparent; -} - -._splitSelect_1lurf_109:hover { - --vscode-dropdown-background: var(--vscode-list-hoverBackground); -} - -/* Action buttons */ -._actions_1lurf_122 { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 4px; - min-width: 56px; -} - -._actionButton_1lurf_130 { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - padding: 0; - background: none; - border: none; - border-radius: 4px; - color: var(--vscode-descriptionForeground); - cursor: pointer; - opacity: 0.6; - transition: - opacity 0.15s ease, - background-color 0.15s ease; -} - -._actionButton_1lurf_130:hover:not(:disabled) { - opacity: 1; - background: var(--vscode-list-hoverBackground); -} - -._actionButton_1lurf_130:disabled { - opacity: 0.3; - cursor: default; -} - -/* Transcript details (below ID link) */ -._detailsRow_1lurf_159 { - color: var(--vscode-descriptionForeground); - font-size: 0.8rem; - line-height: 1.2; - padding-bottom: 1px; -} - -/* Warning for missing transcripts */ -._notFoundRow_1lurf_167 { - display: flex; - align-items: center; - gap: 6px; - color: var(--vscode-editorWarning-foreground, #cca700); - font-size: 0.8rem; - line-height: 1.2; - padding-bottom: 1px; -} - -._notFoundRow_1lurf_167 i { - font-size: 0.75rem; -} - -/* Modal styles */ -._modalContent_1lurf_182 { - display: flex; - flex-direction: column; - gap: 12px; -} - -._modalContent_1lurf_182 p { - margin: 0; -} - -._warning_1lurf_192 { - color: var(--vscode-errorForeground); - font-size: 0.9rem; -} - -._modalButton_1lurf_197 { - padding: 6px 14px; - border: 1px solid var(--vscode-button-border, transparent); - border-radius: 2px; - background: var(--vscode-button-secondaryBackground); - color: var(--vscode-button-secondaryForeground); - cursor: pointer; - font-size: 13px; -} - -._modalButton_1lurf_197:hover { - background: var(--vscode-button-secondaryHoverBackground); -} - -._modalButtonPrimary_1lurf_211 { - background: var(--vscode-button-background); - color: var(--vscode-button-foreground); -} - -._modalButtonPrimary_1lurf_211:hover { - background: var(--vscode-button-hoverBackground); -} - -._modalButtonPrimary_1lurf_211:disabled { - opacity: 0.5; - cursor: default; -} -/* Modal styles for the "New Split" dialog */ -._modalContent_cmqyy_2 { - display: flex; - flex-direction: column; - gap: 12px; -} - -._modalContent_cmqyy_2 p { - margin: 0; -} - -._modalButton_cmqyy_12 { - padding: 6px 14px; - border: 1px solid var(--vscode-button-border, transparent); - border-radius: 2px; - background: var(--vscode-button-secondaryBackground); - color: var(--vscode-button-secondaryForeground); - cursor: pointer; - font-size: 13px; -} - -._modalButton_cmqyy_12:hover { - background: var(--vscode-button-secondaryHoverBackground); -} - -._modalButtonPrimary_cmqyy_26 { - background: var(--vscode-button-background); - color: var(--vscode-button-foreground); -} - -._modalButtonPrimary_cmqyy_26:hover { - background: var(--vscode-button-hoverBackground); -} - -._modalButtonPrimary_cmqyy_26:disabled { - opacity: 0.5; - cursor: default; -} -._container_1xvfl_1 { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - height: 100%; - width: 100%; -} - -/* Grid layout for header and rows */ -._gridContainer_1xvfl_11 { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - overflow: hidden; -} - -/* Grid header - fully enclosed */ -._header_1xvfl_20 { - display: grid; - /* grid-template-columns set dynamically via inline style */ - align-items: center; - gap: 12px; - padding: 0 1rem; - height: 28px; - background: var(--vscode-sideBarSectionHeader-background); - border-bottom: solid var(--bs-light-border-subtle) 1px; - font-size: 0.75rem; - font-weight: 500; - line-height: 28px; - color: var(--vscode-descriptionForeground); - text-transform: uppercase; - letter-spacing: 0.5px; - position: sticky; - top: 0; - z-index: 1; - user-select: none; -} - -._headerCheckbox_1xvfl_41 { - display: flex; - align-items: center; - justify-content: center; - transform: scale(0.85); -} - -._headerTranscript_1xvfl_48 { - display: flex; - align-items: center; - gap: 12px; -} - -/* Bulk actions in header */ -._bulkActions_1xvfl_55 { - display: inline-flex; - align-items: center; - gap: 1px; - margin-left: 12px; -} - -._selectedCount_1xvfl_62 { - font-weight: 600; - text-transform: none; - letter-spacing: normal; - color: var(--vscode-foreground); - margin-right: 8px; -} - -._bulkButton_1xvfl_70 { - text-transform: none; - letter-spacing: normal; - padding: 2px 4px !important; - font-size: 10px !important; - height: 24px !important; - min-height: 24px !important; -} - -._bulkActions_1xvfl_55 vscode-button::part(base) { - padding: 2px 6px !important; -} - -._bulkButton_1xvfl_70 i { - margin-right: 4px; -} - -/* Responsive: Icons only at narrow widths */ -@media (max-width: 900px) { - ._buttonText_1xvfl_89 { - display: none; - } - - ._bulkButton_1xvfl_70 i { - margin-right: 0; - } - - ._selectedCount_1xvfl_62 { - margin-right: 4px; - } -} - -._headerLabels_1xvfl_102 { - text-align: right; - min-width: 80px; - padding-right: 8px; -} - -._headerTarget_1xvfl_108 { - text-align: right; - min-width: 80px; - padding-right: 8px; -} - -._headerSplit_1xvfl_114 { - text-align: center; -} - -._headerActions_1xvfl_118 { - display: flex; - justify-content: flex-start; - min-width: 56px; -} - -/* Scrollable list */ -._list_1xvfl_125 { - display: flex; - flex-direction: column; - gap: 0; - padding: 0; - overflow-y: auto; - flex: 1; -} - -._emptyState_1xvfl_134 { - color: var(--vscode-descriptionForeground); - text-align: center; - padding: 24px 1rem; -} - -/* Modal styles */ -._modalContent_1xvfl_141 { - display: flex; - flex-direction: column; - gap: 12px; -} - -._modalContent_1xvfl_141 p { - margin: 0; - color: var(--vscode-foreground); -} - -._splitSelector_1xvfl_152 { - display: flex; - flex-direction: column; - gap: 8px; -} - -._customInput_1xvfl_158 { - margin-top: 4px; -} - -._warning_1xvfl_162 { - color: var(--vscode-errorForeground); - font-size: 0.9em; -} -._container_4r2t1_1 { - position: relative; - display: flex; - flex-direction: column; -} - -/* Trigger button (collapsed state) */ -._trigger_4r2t1_8 { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - width: fit-content; - padding: 3px 3px; - background: var(--vscode-dropdown-background, var(--vscode-input-background)); - border: 1px solid - var(--vscode-dropdown-border, var(--vscode-input-border, #3c3c3c)); - border-radius: 2px; - color: var(--vscode-dropdown-foreground, var(--vscode-foreground)); - cursor: pointer; - text-align: left; - font-family: inherit; - font-size: inherit; -} - -._chevron_4r2t1_26 { - flex-shrink: 0; - font-size: 12px; - opacity: 0.6; - line-height: 1; - transform: rotate(180deg); -} - -._trigger_4r2t1_8:hover { - background: var(--vscode-dropdown-background); - border-color: var(--vscode-focusBorder); -} - -._trigger_4r2t1_8:focus { - outline: none; - border-color: var(--vscode-focusBorder); -} - -._triggerContent_4r2t1_44 { - display: flex; - flex-direction: column; -} - -._triggerSizer_4r2t1_49 { - font-size: var(--vscode-font-size, 13px); - white-space: nowrap; - height: 0; - overflow: hidden; - display: block; -} - -._triggerPrimary_4r2t1_57 { - font-size: var(--vscode-font-size, 13px); - white-space: nowrap; -} - -._triggerSecondary_4r2t1_62 { - font-size: calc(var(--vscode-font-size, 13px) - 2px); - color: var(--vscode-descriptionForeground); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - opacity: 0.7; -} - -._triggerPlaceholder_4r2t1_71 { - color: var(--vscode-input-placeholderForeground); -} - -/* Dropdown panel - rendered via portal */ -._dropdown_4r2t1_76 { - z-index: 10000; - background: var( - --vscode-dropdown-background, - var(--vscode-input-background, #252526) - ); - border: 1px solid - var(--vscode-dropdown-border, var(--vscode-input-border, #3c3c3c)); - border-radius: 2px; - max-height: 200px; - overflow-y: auto; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); -} - -/* Dropdown items */ -._item_4r2t1_91 { - padding: 6px 10px; - cursor: pointer; - border-bottom: 1px solid var(--vscode-panel-border); -} - -._item_4r2t1_91:last-child { - border-bottom: none; -} - -._item_4r2t1_91:hover { - background-color: var(--vscode-list-hoverBackground); -} - -/* Selected item - with proper contrast */ -._item_4r2t1_91._selected_4r2t1_106 { - background-color: var(--vscode-list-activeSelectionBackground); -} - -._item_4r2t1_91._selected_4r2t1_106 ._primaryText_4r2t1_110, -._item_4r2t1_91._selected_4r2t1_106 ._secondaryText_4r2t1_111 { - color: var(--vscode-list-activeSelectionForeground); -} - -._primaryText_4r2t1_110 { - color: var(--vscode-foreground); - font-size: var(--vscode-font-size, 13px); -} - -._secondaryText_4r2t1_111 { - color: var(--vscode-descriptionForeground); - font-size: calc(var(--vscode-font-size, 13px) - 2px); - margin-top: 1px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - opacity: 0.7; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -._loading_4r2t1_133 { - color: var(--vscode-descriptionForeground); - font-style: italic; -} - -._error_4r2t1_138 { - color: var(--vscode-errorForeground); -} - -/* Divider between sets and create option */ -._divider_4r2t1_143 { - height: 1px; - background: var(--vscode-editorWidget-border, var(--vscode-panel-border)); - margin: 4px 10px; -} - -/* Remove border from item before divider */ -._item_4r2t1_91:has(+ ._divider_4r2t1_143) { - border-bottom: none; -} - -/* Create option styling */ -._createOption_4r2t1_155 { - margin-top: 2px; -} - -._createOption_4r2t1_155 ._primaryText_4r2t1_110 { - color: var(--vscode-textLink-foreground); - display: flex; - align-items: center; - gap: 6px; -} - -._createOption_4r2t1_155 ._primaryText_4r2t1_110::before { - content: "+"; - font-weight: 600; - font-size: 14px; -} - -/* Modal content styles */ -._modalContent_4r2t1_173 { - display: flex; - flex-direction: column; - gap: 12px; -} - -._modalContent_4r2t1_173 p { - margin: 0; -} - -._hint_4r2t1_183 { - font-size: 0.8rem; - color: var(--vscode-descriptionForeground); - font-style: italic; -} - -/* Modal button styles */ -._modalButton_4r2t1_190 { - padding: 6px 14px; - border: 1px solid var(--vscode-button-border, transparent); - border-radius: 2px; - background: var(--vscode-button-secondaryBackground); - color: var(--vscode-button-secondaryForeground); - cursor: pointer; - font-size: 13px; -} - -._modalButton_4r2t1_190:hover { - background: var(--vscode-button-secondaryHoverBackground); -} - -._modalButton_4r2t1_190:disabled { - opacity: 0.5; - cursor: default; -} - -._modalButtonPrimary_4r2t1_209 { - background: var(--vscode-button-background); - color: var(--vscode-button-foreground); -} - -._modalButtonPrimary_4r2t1_209:hover { - background: var(--vscode-button-hoverBackground); -} -._container_1ph4q_1 { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - color: var(--vscode-descriptionForeground); - font-size: 14px; -} - -._separator_1ph4q_10 { - color: var(--vscode-panel-border); -} -._container_12me1_1 { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; -} - -._headerRow_12me1_8 { - display: flex; - align-items: center; - gap: 16px; - padding: 1rem 1.25rem 0.75rem 1rem; - flex-shrink: 0; -} - -._title_12me1_16 { - font-size: 16px; - font-weight: 600; - color: var(--vscode-foreground); - margin: 0; - flex-shrink: 0; -} - -._spacer_12me1_24 { - flex: 1; -} - -._headerActions_12me1_28 { - display: flex; - align-items: center; - gap: 4px; -} - -/* Filter controls in header */ -._filterControls_12me1_35 { - display: flex; - align-items: center; - gap: 12px; -} - -._searchInput_12me1_41 { - width: 180px; - font-size: 0.85rem !important; -} - -._searchInput_12me1_41 input { - font-size: 0.85rem !important; -} - -._splitFilter_12me1_50 { - display: flex; - align-items: center; - gap: 6px; -} - -._filterLabel_12me1_56 { - color: var(--vscode-descriptionForeground); - font-size: 0.85rem; -} - -._splitSelect_12me1_61 { - width: 80px !important; - min-width: 80px !important; - max-width: 80px !important; -} - -._iconButton_12me1_67 { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - padding: 0; - background: transparent; - border: none; - border-radius: 4px; - color: var(--vscode-descriptionForeground); - cursor: pointer; - transition: - background-color 0.1s, - color 0.1s; -} - -._iconButton_12me1_67:hover { - background: var(--vscode-toolbar-hoverBackground); - color: var(--vscode-foreground); -} - -._iconButton_12me1_67:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -._modalContent_12me1_94 { - display: flex; - flex-direction: column; - gap: 12px; -} - -._modalContent_12me1_94 p { - margin: 0; -} - -._warning_12me1_104 { - color: var(--vscode-errorForeground); - font-size: 13px; -} - -._renameInput_12me1_109 { - width: 100%; -} - -._content_12me1_113 { - flex: 1; - display: flex; - flex-direction: column; - gap: 0; - overflow: hidden; - padding: 0; -} - -._casesSection_12me1_122 { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; -} - -._loading_12me1_129 { - color: var(--vscode-descriptionForeground); - font-style: italic; - padding: 16px 0; -} - -._error_12me1_135 { - color: var(--vscode-errorForeground); - padding: 16px 0; -} - -._emptyState_12me1_140 { - color: var(--vscode-descriptionForeground); - text-align: center; - padding: 48px 16px; -} - -._casesPlaceholder_12me1_146 { - padding: 16px; - background: var(--vscode-editor-background); - border: 1px solid var(--vscode-panel-border); - border-radius: 4px; - color: var(--vscode-descriptionForeground); -} - -/* Summary wrapper for responsive hiding */ -._summaryWrapper_12me1_155 { - flex-shrink: 0; -} - -/* Responsive: Hide summary at medium widths */ -@media (max-width: 1150px) { - ._summaryWrapper_12me1_155 { - display: none; - } -} - -/* Responsive: Wrap header at narrow widths */ -@media (max-width: 900px) { - ._headerRow_12me1_8 { - flex-wrap: wrap; - } - - ._spacer_12me1_24 { - display: none; - } - - ._filterControls_12me1_35 { - width: 100%; - margin-top: 4px; - order: 10; - } -} -._activityBar_10a5h_1 { - width: 78px; - border-right: solid 1px var(--bs-border-color); - background-color: var(--bs-light); -} - -._activityHost_10a5h_7 { - padding-top: 1rem; - padding-bottom: 1rem; - display: grid; - justify-content: center; - grid-auto-rows: max-content; - row-gap: 1rem; -} - -._activity_10a5h_1 { - display: flex; - flex-direction: column; - align-items: center; - flex-shrink: 1; - padding: 0.3rem; - border-radius: var(--bs-border-radius); -} - -._activity_10a5h_1:hover { - background-color: var(--bs-body-bg); - cursor: pointer; -} - -._activity_10a5h_1._selected_10a5h_30 { - background-color: var(--bs-primary-bg-subtle); -} - -._icon_10a5h_34 { - font-size: 1.1rem; -} - -._label_10a5h_38 { - font-size: 0.5rem; - text-transform: uppercase; -} -._outerLayout_1j111_1 { - display: flex; - flex-direction: column; - height: 100vh; - width: 100vw; - overflow: hidden; -} - -._layout_1j111_9 { - display: flex; - flex-direction: row; - flex: 1; - overflow: hidden; -} - -._content_1j111_16 { - flex: 1; - overflow: hidden; -} - -._content_1j111_16._scrolling_1j111_21 { - overflow: auto; -} -._projectBar_cmger_1 { - display: flex; - flex-direction: column; - background: var(--bs-light); - border-bottom: solid 1px var(--bs-border-color); - min-height: 2em; - padding: 0.1rem 0.3rem; - row-gap: 0; - align-items: center; - justify-content: center; -} - -._row_cmger_13 { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - column-gap: 0.5rem; - width: 100%; - height: 100%; -} - -._left_cmger_22, -._center_cmger_23, -._right_cmger_24 { - font-size: var(--inspect-font-size-small); -} - -._left_cmger_22 { - grid-column: 1; - justify-self: start; - display: flex; - align-items: center; - gap: 0.2rem; -} - -._center_cmger_23 { - grid-column: 2; - justify-self: center; - display: flex; - align-items: center; -} - -._right_cmger_24 { - grid-column: 3; - justify-self: end; - display: flex; - align-items: center; -} - -._historyButton_cmger_50 { - font-size: 0.9em; -} - -._navButton_cmger_54 { - display: inline-flex; - align-items: center; - justify-content: center; - border: none; - background: transparent; - color: var(--bs-secondary); - padding: 0.12rem 0.3rem; - border-radius: var(--bs-border-radius); -} - -._navButton_cmger_54:hover, -._navButton_cmger_54:focus-visible { - background-color: var(--bs-secondary-bg-subtle); -} -._container_1ch30_1 { - height: 100%; - display: grid; - grid-template-rows: max-content auto 1fr; -} - -._container_1ch30_1 > ._headerRow_1ch30_7 { - grid-row: 1; -} - -._container_1ch30_1 > ._conflictBanner_1ch30_11 { - grid-row: 2; -} - -._container_1ch30_1 > ._loading_1ch30_15, -._container_1ch30_1 > ._error_1ch30_16 { - grid-row: 3; -} - -._container_1ch30_1 > ._splitLayout_1ch30_20 { - grid-row: 3; -} - -._headerRow_1ch30_7 { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 1.25rem 0.75rem 1rem; - border-bottom: 1px solid var(--vscode-widget-border); -} - -._header_1ch30_7 { - font-size: 16px; - font-weight: 600; - color: var(--vscode-foreground); -} - -._detail_1ch30_38 { - font-size: 12px; - color: var(--vscode-descriptionForeground); -} - -._conflictBanner_1ch30_11 { - background: var(--vscode-inputValidation-warningBackground); - border: 1px solid var(--vscode-inputValidation-warningBorder); - padding: 8px 12px; - margin: 0.5rem 1rem; - border-radius: 4px; - display: flex; - justify-content: space-between; - align-items: center; - font-size: 13px; - color: var(--vscode-foreground); -} - -._conflictActions_1ch30_56 { - display: flex; - gap: 8px; -} - -._splitLayout_1ch30_20 { - display: flex; - min-height: 0; - gap: 1rem; - padding-left: 1rem; - padding-top: 1rem; -} - -._treeNav_1ch30_69 { - width: 160px; - flex-shrink: 0; - overflow-y: auto; -} - -._navList_1ch30_75 { - list-style: none; - margin: 0; - padding: 0; -} - -._navListItem_1ch30_81 { - list-style: none; - margin: 0; - padding: 0; -} - -._navGroup_1ch30_87 { - font-size: 12px; - font-weight: 600; - color: var(--vscode-foreground); - padding: 6px 8px 4px 8px; - text-transform: uppercase; - letter-spacing: 0.03em; -} - -._navGroup_1ch30_87:not(:first-child) { - margin-top: 0.75rem; -} - -._navItem_1ch30_100 { - display: block; - width: 100%; - padding: 4px 8px 4px 16px; - font-size: 13px; - color: var(--vscode-foreground); - background: none; - border: none; - text-align: left; - cursor: pointer; - border-radius: 3px; -} - -._navItem_1ch30_100:hover { - background: var(--vscode-list-hoverBackground); -} - -._navItem_1ch30_100:focus { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; -} - -._scrollContent_1ch30_122 { - flex: 1; - min-width: 0; - overflow-y: auto; - padding-right: 1rem; -} - -/* Make text fields wider */ -._field_1ch30_130 vscode-textfield { - width: 400px !important; -} - -._field_1ch30_130 vscode-textfield[type="number"] { - width: 250px !important; -} - -._field_1ch30_130 vscode-textarea { - width: 400px !important; -} - -._field_1ch30_130 vscode-single-select { - width: 250px !important; -} - -._section_1ch30_146 { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-bottom: 1.5rem; -} - -._sectionHeader_1ch30_153 { - font-size: 13px; - font-weight: 600; - color: var(--vscode-foreground); - padding: 0.35rem 0.75rem 0.35rem 1rem; - margin-left: -0.75rem; - background: var(--vscode-sideBarSectionHeader-background); - border-radius: 3px; -} - -._field_1ch30_130 { - display: flex; - flex-direction: column; -} - -._loading_1ch30_15, -._error_1ch30_16 { - font-size: 13px; - color: var(--vscode-foreground); -} - -._error_1ch30_16 { - color: var(--vscode-errorForeground); -} -._root_1swht_1 { - height: 100%; - display: grid; - grid-template-rows: max-content max-content max-content max-content 1fr; -} -._gridWrapper_bykj9_1 { - height: 100%; - width: 100%; -} - -/* Hide sort index numbers in multi-column sort */ -._gridWrapper_bykj9_1 .ag-header-cell .ag-sort-order { - display: none; -} - -/* Set line height for grid cells */ -._gridWrapper_bykj9_1 .ag-cell { - line-height: 1.1em; -} - -/* Row numbers cell styling */ -._gridWrapper_bykj9_1 .ag-cell.row-number-cell { - cursor: pointer; -} - -._gridWrapper_bykj9_1 .ag-cell.row-number-cell:hover { - text-decoration: underline; -} -.json-panel { - width: 100%; - padding: 0.5em; -} - -.json-panel:not(.simple) { - background: var(--bs-light); - border-radius: var(--bs-border-radius); -} - -.json-panel .source-code { - font-size: var(--inspect-font-size-small); - white-space: pre-wrap !important; - word-wrap: anywhere !important; -} - -/* Prism.js theme customizations can go here if needed */ -.language-javascript { - background: transparent !important; -} -._rootControl_19z32_1 { - background-color: var(--bs-body-bg); - border-radius: var(--bs-border-radius); - display: flex; - flex-direction: row; - align-items: center; - border: solid 1px var(--bs-border-color); -} - -._segment_19z32_10._selected_19z32_10 { - background-color: var(--bs-secondary-bg); - border-radius: var(--bs-border-radius); -} - -button._segment_19z32_10 { - margin: 1px; - background-color: var(--bs-body-bg); - border-radius: var(--bs-border-radius); - border: none; - padding-block-end: 0; - padding-block-start: 0; - padding-inline-start: 5px; - padding-inline-end: 5px; - height: calc(100% - 2px); -} - -._segment_19z32_10 i { - margin-right: 0.2em; -} -._tabs_tfvnu_1 { - align-items: center; -} - -._tabContents_tfvnu_5 { - flex: 1; - overflow-y: hidden; -} - -._tabContents_tfvnu_5._scrollable_tfvnu_10 { - min-height: 0; - overflow-y: auto; - height: 100%; -} - -._tab_tfvnu_1 { - color: "var(--bs-body-color)"; - padding: 0.25rem 0.5rem; - border-top-left-radius: var(--bs-border-radius); - border-top-right-radius: var(--bs-border-radius); - font-weight: 500; - margin-top: 2px; - margin-bottom: -1px; -} - -._tabItem_tfvnu_26 { - align-self: end; -} - -._tabIcon_tfvnu_30 { - margin-right: 0.5em; -} - -._tabTools_tfvnu_34 { - flex-basis: auto; - margin-left: auto; - display: flex; - align-items: center; - justify-content: end; - flex-wrap: wrap; - row-gap: 0.3rem; -} - -._tabStyle_tfvnu_44 { - padding-left: 0.7em; - padding-right: 0.7em; -} - -._tabTools_tfvnu_34 > * { - flex: 0 1 auto; - margin-left: 0.5rem; -} - -._tabTools_tfvnu_34 input { - font-size: var(--inspect-font-size-smallest); -} - -._tabTools_tfvnu_34 .btn { - font-size: var(--inspect-font-size-smallest); - padding: 0.1em 0.5em; -} -.card-header-container:not(.card-header-modern) { - display: grid; - grid-template-columns: max-content auto; - column-gap: 0rem; - padding: 0.5rem 0.5rem 0.5rem 0.5rem; - font-size: var(--inspect-font-size-small); - font-weight: 600; - border-bottom: solid 1px var(--bs-light-border-subtle); -} - -.card-header-container.card-header-modern { - display: grid; - grid-template-columns: max-content auto; - column-gap: 0rem; - font-size: var(--inspect-font-size-small); - color: var(--bs-secondary); - padding: 0.5rem 0.5rem 0rem 0.5rem; -} - -.card-header-icon:not(.card-header-icon-empty) { - padding-right: 0.2rem; -} - -.card-body { - background-color: var(--bs-body-bg); - padding: 0.5rem !important; -} - -.card { - background-color: var(--bs-light-bg-subtle); - border: solid 1px var(--bs-light-border-subtle); - border-radius: var(--bs-border-radius); - overflow: hidden; -} - -.card-collaping-header { - border-bottom: none; -} - -.card-collapsing-header-container { - justify-content: space-between; - align-items: center; -} - -.card-collapsing-header-icon { - flex: 0 0 content; - padding-right: 0.5rem; -} - -.card-collapsing-header-contents { - color: var(--body-color); - opacity: 0.8; - flex: 1 1 auto; - font-size: var(--inspect-font-size-smaller); - padding-right: 0; - padding-left: 0; - transition: opacity 0.2s ease-out; - display: flex; - justify-content: space-between; -} - -.card-collapsing-header-toggle { - flex: 0 1 1em; - text-align: right; - padding: 0 0.5em 0.1em 0.5em; - font-size: var(--inspect-font-size-smaller); -} - -.card-body.card-no-padding { - padding: 0; -} -._grid_1vh2w_1 { - display: grid; - grid-template-columns: max-content auto; - column-gap: 0.5em; - row-gap: 0.2em; -} - -._cell_1vh2w_8 { - font-weight: 400; - white-space: nowrap; -} - -._value_1vh2w_13 { - overflow-wrap: anywhere; -} -._ansiDisplayContainer_sawhg_1 { - position: relative; - width: 100%; -} - -._ansiDisplay_sawhg_1 { - font-family: monospace; - white-space: pre-wrap; - line-height: normal; - --ansiBlack: #000000; - --ansiRed: #cd3131; - --ansiGreen: #00bc00; - --ansiYellow: #949800; - --ansiBlue: #0451a5; - --ansiMagenta: #bc05bc; - --ansiCyan: #0598bc; - --ansiWhite: #555555; - --ansiBrightBlack: #666666; - --ansiBrightRed: #cd3131; - --ansiBrightGreen: #14ce14; - --ansiBrightYellow: #b5ba00; - --ansiBrightBlue: #0451a5; - --ansiBrightMagenta: #bc05bc; - --ansiBrightCyan: #0598bc; - --ansiBrightWhite: #a5a5a5; -} - -._ansiDisplayRaw_sawhg_28 { - margin: 0; - white-space: pre-wrap; -} - -._ansiDisplayToggle_sawhg_33 { - position: absolute; - top: 0.25rem; - right: 0.25rem; - z-index: 10; - opacity: 0.7; - transition: opacity 0.2s; -} - -._ansiDisplayContainer_sawhg_1:hover ._ansiDisplayToggle_sawhg_33 { - opacity: 1; -} - -.dark-mode ._ansiDisplay_sawhg_1 { - --ansiBlack: #000000; - --ansiRed: #cd3131; - --ansiGreen: #0dbc79; - --ansiYellow: #e5e510; - --ansiBlue: #2472c8; - --ansiMagenta: #bc3fbc; - --ansiCyan: #11a8cd; - --ansiWhite: #e5e5e5; - --ansiBrightBlack: #666666; - --ansiBrightRed: #f14c4c; - --ansiBrightGreen: #23d18b; - --ansiBrightYellow: #f5f543; - --ansiBrightBlue: #3b8eea; - --ansiBrightMagenta: #d670d6; - --ansiBrightCyan: #29b8db; - --ansiBrightWhite: #e5e5e5; -} - -@keyframes _ansi-display-run-blink_sawhg_1 { - 50% { - opacity: 0; - } -} - -._ansiDisplayToggle_sawhg_33 { - padding: 0.1rem; - padding-left: 0.5rem; - background-color: var(--bs-body-bg); -} -._visible_tm52u_1 { - display: block; -} - -._hidden_tm52u_5 { - display: none; -} - -._pills_tm52u_9 { - margin-right: 0; -} - -._pill_tm52u_9 { - min-width: 4rem; - font-size: var(--inspect-font-size-small); - padding: 0.1rem 0.6rem; - border-radius: var(--bs-border-radius); -} -._expandablePanel_1ka1g_1 { - position: relative; -} - -._expandableBordered_1ka1g_5 { - border: solid var(--bs-light-border-subtle) 1px; - padding: 0.5em; -} - -._expandableTogglable_1ka1g_10 { - margin-bottom: 1em; -} - -._expandableContents_1ka1g_14 { - font-size: var(--inspect-font-size-base); -} - -._expandableCollapsed_1ka1g_18 { - overflow: hidden; -} - -._moreToggle_1ka1g_22 { - display: flex; - margin-top: 0; - height: 20px; - background-color: var(--bs-body-bg); - border-radius: 5px; - border: solid var(--bs-light-border-subtle) 1px; - color: var(--bs-link-color); -} - -._moreToggle_1ka1g_22._bordered_1ka1g_32 { - border-top: solid var(--bs-light-border-subtle) 1px; -} - -._moreToggleButton_1ka1g_36 { - font-size: var(--inspect-font-size-smaller); - border: none; - padding: 0rem 0.5rem; - background-color: transparent; - color: var(--bs-body-color); -} - -._separator_1ka1g_44 { - height: 1px; - background-color: var(--bs-light-border-subtle); - margin-top: -1px; -} - -._moreToggle_1ka1g_22._inlineRight_1ka1g_50 { - position: absolute; - bottom: 0.25em; - right: 0.25em; -} - -._moreToggle_1ka1g_22._blockLeft_1ka1g_56 { - width: fit-content; - margin-top: 0.2em; - margin-bottom: 0.5em; -} -._keyPairContainer_qjlxf_1 { - display: grid; - grid-template-columns: max-content auto; - column-gap: 0.5em; - padding-top: 4px; - padding-bottom: 4px; -} - -._keyPairBordered_qjlxf_9 { - border-bottom: solid 1px var(--bs-border-color); -} - -._key_qjlxf_1 { - display: grid; - grid-template-columns: 1em auto; - cursor: pointer; -} - -._pre_qjlxf_19 { - margin-bottom: 0; -} - -._treeIcon_qjlxf_23 { - margin-top: -3px; -} -._copyButton_1goi8_1 { - border: none; - background-color: inherit; - opacity: 0.5; - padding-top: 0; - transition: opacity 0.2s; -} - -._copyButton_1goi8_1:hover { - opacity: 0.75; -} -._labeledValue_6obbb_1 { - display: flex; - justify-content: flex-start; -} - -._labeledValue_6obbb_1._row_6obbb_6 { - flex-direction: row; -} - -._labeledValue_6obbb_1._column_6obbb_10 { - flex-direction: column; -} - -._labeledValue_6obbb_1._row_6obbb_6 ._labeledValueLabel_6obbb_14 { - margin-right: 0.5em; -} -._message_b8oe1_1 { - font-weight: 300; - margin-left: 0; - margin-right: 0; - white-space: normal; -} - -._systemRole_b8oe1_8 { - opacity: 0.7; -} - -._messageGrid_b8oe1_12 { - display: grid; - grid-template-columns: max-content max-content max-content; - column-gap: 0.3em; - font-weight: 500; - margin-bottom: 0.3em; -} - -._toolMessageGrid_b8oe1_20 { - border-bottom: solid 1px var(--bs-border-color); -} - -._messageContents_b8oe1_24 { - margin-left: 0; - padding-bottom: 0; -} - -._messageContents_b8oe1_24._indented_b8oe1_29 { - margin-left: 0rem; -} - -._copyLink_b8oe1_33 { - opacity: 0; - padding-left: 0; - padding-right: 2em; -} - -._copyLink_b8oe1_33:hover { - opacity: 0.75; -} - -._metadataLabel_b8oe1_43 { - padding-top: 1em; -} - -._hover_b8oe1_47 ._copyLink_b8oe1_33 { - opacity: 0.75; -} -.markdown-ordered-list-item { - margin-bottom: 0.2em; -} - -.markdown-content p:last-child { - margin-bottom: 0; -} -._cite_1t1bm_1 { - color: var(--bs-primary-text-emphasis); - text-decoration: underline; - text-decoration-style: dashed; - cursor: pointer; -} - -._cite_1t1bm_1:hover { - text-decoration-style: solid; - background-color: var(--bs-primary-bg-subtle); -} -._content_13ihw_1 { - white-space: pre-wrap; -} -._title_1pwo3_1 { - margin-bottom: 0.25em; - display: grid; - grid-template-columns: auto 1fr auto; - column-gap: 0.3em; - align-items: baseline; - color: var(--bs-secondary) !important; - font-weight: var(--bs-body-font-weight) !important; -} - -._content_1pwo3_11 { - margin-bottom: 0.5em; - overflow-wrap: anywhere; -} - -._grid_1pwo3_16 { - border: solid var(--bs-border-color) 1px; - border-radius: var(--bs-border-radius); - padding: 0.5em; -} - -._content_1pwo3_11 ._grid_1pwo3_16 .text-style-label { - text-transform: none !important; -} -._contentData_1lrx1_1 { - margin-bottom: 0.5em; -} -._webSearch_1376z_1 { - display: grid; - grid-template-columns: max-content 1fr; - column-gap: 0.5em; - align-items: baseline; -} - -._query_1376z_8 { - font-family: var(--bs-font-monospace); -} -._webSearch_1mixg_1 { - display: grid; - grid-template-columns: max-content 1fr; - column-gap: 0.5em; - align-items: baseline; -} - -._query_1mixg_8 { - font-family: var(--bs-font-monospace); -} - -._result_1mixg_12 a:hover { - text-decoration: underline; -} - -._result_1mixg_12 a { - opacity: 0.8; - text-decoration: none; -} -._documentFrame_1576h_1 { - display: grid; - grid-template-rows: auto 1fr; - row-gap: 0.5em; - padding-top: 1em; - padding-bottom: 1em; -} - -._documentFrameTitle_1576h_9 { - display: grid; - grid-template-columns: auto 1fr; - column-gap: 0.3em; - align-items: center; -} - -._downloadLink_1576h_16:hover { - text-decoration: underline; - cursor: pointer; -} - -._imageDocument_1576h_21 { - max-width: 100%; - max-height: 200px; - object-fit: cover; -} -._jsonMessage_oxf8d_1 { -} -._citations_1ggvf_1 { - margin-top: 1em; - margin-bottom: 1em; - display: grid; - grid-template-columns: max-content 1fr; - column-gap: 0.5em; -} - -a._citationLink_1ggvf_9 { - display: block; - color: var(--bs-body-color); - text-decoration: none; -} -a._citationLink_1ggvf_9:hover { - text-decoration: underline; -} -._contentImage_8rgix_1 { - max-width: 800px; - border: solid var(--bs-border-color) 1px; -} - -._reasoning_8rgix_6 { - border: solid var(--bs-light-border-subtle) 1px; - padding: 1em; - margin-bottom: 0.5em; - background-color: var(--bs-light-bg-subtle); - border-radius: var(--bs-border-radius); -} - -._data_8rgix_14 { - border: solid var(--bs-light-border-subtle) 1px; - padding: 1em; - margin-bottom: 0.5em; -} -._mcpToolUse_1792k_1 { - margin-left: 0.5em; - margin-top: 2em; - margin-bottom: 2em; - padding: 0.25em 0.75em; - border-left: solid 5px var(--bs-border-color); -} - -._title_1792k_9 { - margin-bottom: 0.25em; - display: grid; - grid-template-columns: auto 1fr auto; - column-gap: 0.3em; - align-items: baseline; - border-bottom: solid 1px var(--bs-border-color); -} - -._titleText_1792k_18 { - margin-bottom: 0; -} - -._args_1792k_22 { - display: grid; - grid-template-columns: auto 1fr; - column-gap: 0.5em; - row-gap: 0.1em; - margin-bottom: 1em; - align-items: baseline; -} - -._argLabel_1792k_31 pre { - margin: 0; -} - -._error_1792k_35 { - color: red; - font-weight: bold; -} - -._toolPanel_1792k_40 { - display: contents; -} -._toolImage_qu6a9_1 { - max-width: 800px; - border: solid var(--bs-border-color) 1px; -} - -._output_qu6a9_6 { - display: grid; -} - -._textOutput_qu6a9_10 { - padding-left: 2px; - padding: 0.5em 0.5em 0.5em 0.5em; - white-space: pre-wrap; - margin-bottom: 0; - font-size: var(--inspect-font-size-smallest) !important; -} - -._textCode_qu6a9_18 { - word-wrap: anywhere; -} -._grid_17ltx_1 { - display: grid; - grid-template-columns: max-content auto; - column-gap: 0.4rem; - row-gap: 0; -} - -._number_17ltx_8 { - margin-top: 0.1em; -} - -._user_17ltx_12 { - background-color: var(--bs-secondary-bg); - border-radius: var(--bs-border-radius); - border: solid 1px var(--bs-light-border-subtle); -} - -._container_17ltx_18 { - padding-left: 0.7rem; - padding-right: 0.7rem; - padding-top: 0.5rem; - border-left: solid 1px var(--bs-light-border-subtle); - border-right: solid 1px var(--bs-light-border-subtle); -} - -._first_17ltx_26 { - border-top-left-radius: var(--bs-border-radius); - border-top-right-radius: var(--bs-border-radius); - border-top: solid 1px var(--bs-light-border-subtle); - padding-top: 0.7rem; -} - -._last_17ltx_33 { - border-bottom-left-radius: var(--bs-border-radius); - border-bottom-right-radius: var(--bs-border-radius); - border-bottom: solid 1px var(--bs-light-border-subtle); - padding-bottom: 0.7rem; -} - -._label_17ltx_40 { - padding-top: 0.7rem; -} - -._highlight_17ltx_44 { - background-color: rgba(var(--bs-info-rgb), 0.12); -} - -._bottomMargin_17ltx_48 { - margin-bottom: 0.5rem; -} -._sourcePanel_bat6y_1 { - width: 100%; - padding: 0.5em; -} - -._sourcePanel_bat6y_1:not(._simple_bat6y_6) { - background: var(--bs-light); - border-radius: var(--bs-border-radius); -} - -._sourcePanel_bat6y_1 ._code_bat6y_11 { - font-size: var(--inspect-font-size-small); - white-space: pre-wrap !important; - word-wrap: anywhere !important; - background: transparent !important; -} -._toolCallView_x6cus_1 { - display: grid; - row-gap: 0.5em; -} -._todoList_1t8rx_1 { - margin-top: 0.5em; - margin-bottom: 0.5em; - display: grid; - grid-template-columns: auto 1fr; - column-gap: 0.3em; -} - -._inProgress_1t8rx_9 { - font-weight: bold; -} -._outputPre_fhwyo_1 { - padding: 0.5em; - margin-top: 0.25em; - margin-bottom: 0; -} - -._toolView_fhwyo_7 { - margin-top: 0.25em; - padding: 0.5em !important; -} - -._toolView_fhwyo_7 pre { - margin-bottom: 0; -} - -._outputCode_fhwyo_16 { - font-size: 0.8rem !important; - overflow-wrap: anywhere !important; - white-space: pre-wrap !important; -} -._image_1vcac_1 { - margin-right: 0.2rem; - opacity: 0.4; -} - -._toolTitle_1vcac_6 { - color: unset !important; -} - -._description_1vcac_10 { - margin-left: 0.3em; - color: var(--bs-secondary-color); -} -._query_seqs2_1 { - margin-bottom: 0.5rem; - font-weight: 500; -} - -._summary_seqs2_6 { - margin-bottom: 0.5rem; -} - -._preWrap_seqs2_10 { - white-space: pre-wrap; - word-break: break-word; -} - -._preCompact_seqs2_15 { - margin-bottom: 0; -} -._container_1q66p_1 { - margin: 0.5em; -} -._grid_hbkjn_1 { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - column-gap: 2em; - row-gap: 0.15em; -} - -._row_hbkjn_8 { - display: flex; - align-items: center; - gap: 0.5em; - cursor: pointer; - border-radius: var(--bs-border-radius); - padding: 0.1em 0.4em; - margin: 0 -0.4em; -} - -._row_hbkjn_8:hover { - background-color: var(--bs-secondary-bg); -} - -._links_hbkjn_22 { - display: flex; - padding-bottom: 0.2em; - margin-bottom: 0.4em; - column-gap: 0.3em; - border-bottom: solid 1px var(--bs-border-color); -} - -._links_hbkjn_22 a { - cursor: pointer; - text-decoration: none; - color: var(--bs-link-color); -} - -._links_hbkjn_22 a:hover { - color: var(--bs-link-hover-color); -} - -._selected_hbkjn_40 { - font-weight: 600; -} -._flex_1kye9_1 { - display: flex; -} - -._label_1kye9_5 { - align-self: center; - margin-right: 0.3em; - margin-left: 0.2em; -} -._flex_1kye9_1 { - display: flex; -} - -._label_1kye9_5 { - align-self: center; - margin-right: 0.3em; - margin-left: 0.2em; -} -._searchBox_fwn6a_1 { - font-size: var(--inspect-font-size-smallest); -} -._progressContainer_1cjjr_1 { - width: 100%; - display: flex; - align-content: flex-start; - justify-content: flex-start; - margin-left: 2.5em; -} - -._progressText_1cjjr_9 { - margin-left: 0.4em; -} -/* PulsingDots.module.css */ -._container_4p85e_2 { - display: inline-flex; - flex-direction: column; - align-items: center; -} - -._dotsContainer_4p85e_8 { - padding-top: 8px; - display: flex; - align-items: center; - justify-content: center; -} - -._small_4p85e_15 ._dotsContainer_4p85e_8 { - column-gap: 2px; -} - -._medium_4p85e_19 ._dotsContainer_4p85e_8 { - column-gap: 5px; - padding-bottom: 2px; -} - -._large_4p85e_24 ._dotsContainer_4p85e_8 { - column-gap: 6px; - padding-bottom: 3px; - padding-left: 2px; -} - -._dot_4p85e_8 { - border-radius: 50%; - display: inline-block; - animation: _pulse_4p85e_1 1.5s ease-in-out infinite; -} - -._subtle_4p85e_36 { - background-color: var(--bs-secondary-bg-subtle); -} - -._primary_4p85e_40 { - background-color: var(--bs-secondary); -} - -._small_4p85e_15 ._dot_4p85e_8 { - width: 3px; - height: 3px; -} - -._medium_4p85e_19 ._dot_4p85e_8 { - width: 8px; - height: 8px; -} - -._large_4p85e_24 ._dot_4p85e_8 { - width: 12px; - height: 12px; -} - -._visuallyHidden_4p85e_59 { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -@keyframes _pulse_4p85e_1 { - 0%, - 100% { - transform: scale(0.7); - opacity: 0.4; - } - 50% { - transform: scale(1); - opacity: 0.8; - } -} -._row_q8zt3_1 { - border-bottom: solid 1px var(--bs-border-color); - padding: 0.25rem 1rem; - background-color: rgba(var(--bs-secondary-rgb), 0.03); -} -._header_u9u40_1 { - border-bottom: solid var(--bs-light-border-subtle) 1px; - padding: 0.3rem 1rem; - user-select: none; -} - -._center_u9u40_7 { - justify-self: center; -} - -._shrinkable_u9u40_11 { - min-width: 0; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -._clickable_u9u40_19 { - cursor: pointer; -} -._container_1quph_1 { - height: 100%; - width: 100%; -} -._error_1rpqv_1 i.bi:before { - color: var(--bs-danger) !important; - margin-right: 0.3rem; -} - -._refusal_1rpqv_6 i.bi:before { - color: var(--bs-primary) !important; - margin-right: 0.3rem; -} -._result_mi1dv_1 { - display: inline-flex; - justify-content: center; - align-items: center; - border-radius: 5px; - padding: 8px 4px; - height: 1rem; - font-size: 0.7rem; -} - -._true_mi1dv_11 { - border: solid var(--bs-success) 1px; - color: var(--bs-success); -} - -._false_mi1dv_16 { - border: solid var(--bs-danger) 1px; - color: var(--bs-danger); -} - -._resultContainer_mi1dv_21 { - display: grid; - grid-template-columns: max-content max-content; - align-items: center; - column-gap: 0.2rem; -} - -._targetValue_mi1dv_28 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - border: none; - margin-left: 0.4rem; -} -._boolean_8citi_1 { - display: inline-flex; - justify-content: center; - align-items: center; - border-radius: 5px; - padding: 8px; - margin-bottom: 1px; - width: 2.5rem; - height: 1rem; - font-size: var(--inspect-font-size-smallest); -} - -._true_8citi_13 { - background-color: var(--bs-success); - border: solid var(--bs-success) 1px; - color: var(--bs-body-bg); -} - -._false_8citi_19 { - background-color: var(--bs-danger); - border: solid var(--bs-danger) 1px; - color: var(--bs-body-bg); -} - -._valueTable_8citi_25 { - display: grid; - grid-template-columns: minmax(auto, max-content) auto; - column-gap: 1rem; - row-gap: 0rem; -} - -._valueKey_8citi_32 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -._inline_8citi_38 ._valueValue_8citi_38 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-align: end; -} - -._inline_8citi_38 ._valueValue_8citi_38 p { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -pre._value_8citi_25 { - margin: 0; -} -._title_19l1b_1 { - margin-left: 0.5em; - display: grid; - grid-template-columns: max-content max-content minmax(0, 1fr); - column-gap: 0.5em; -} - -._contents_19l1b_8 { - padding: 0.4em; - margin-bottom: 0; - border: solid 1px var(--bs-light-border-subtle); - border-radius: var(--bs-border-radius); -} -._panel_8zdtn_1 { - margin: 0.5em 0; -} -._tab_1je38_1 { - min-width: 4rem; - padding: 0.1rem 0.6rem; - border-radius: var(--bs-border-radius); -} -._navs_1vm6p_1 { - margin-right: 0; -} -._label_1nn7f_1 { - margin-right: 0.2em; - justify-self: end; -} - -._navs_1nn7f_6 { - justify-self: end; - display: flex; - flex-direction: columns; -} - -._card_1nn7f_12 { - position: relative; - background-color: var(--bs-body-bg); - padding: 0.625rem; - border: solid 1px var(--bs-light-border-subtle); - border-radius: var(--bs-border-radius); -} - -._cardContent_1nn7f_20 { - padding: 0; - display: inherit; -} - -._cardContent_1nn7f_20._hidden_1nn7f_25 { - display: none; -} - -._hidden_1nn7f_25 { - display: none; -} - -._copyLink_1nn7f_33 { - font-size: 1.2em; - height: 1em; - opacity: 0; - padding-left: 0.2em; - padding-right: 2em; -} - -._hover_1nn7f_41 ._copyLink_1nn7f_33 { - opacity: 0.75; -} - -._root_1nn7f_45 { - background-color: var(--bs-light-bg-subtle); - border-radius: unset; -} - -._bottomDongle_1nn7f_50 { - display: block; - position: absolute; - margin: 0 auto; - width: fit-content; - bottom: -10px; - left: 50%; - transform: translateX(-50%); - background-color: var(--bs-body-bg); - border: solid 1px var(--bs-light-border-subtle); - color: var(--bs-secondary); - - border-radius: var(--bs-border-radius); - padding: 0em 0.4em; - cursor: pointer; -} - -._dongleIcon_1nn7f_67 { - padding-right: 0.3em; -} -._panel_vz394_1 { - margin: 0.2em 0; -} -._grid_1eq5o_1 { - width: 100%; - display: grid; - grid-template-columns: 1fr max-content; - column-gap: 1em; -} - -._jsonPanel_1eq5o_8 { - padding: 0 !important; -} -._wrapper_sq96g_1 { - display: grid; - grid-template-columns: 0 auto auto; - column-gap: 1.5em; - row-gap: 0.2em; -} - -._col2_sq96g_8 { - grid-column: 2; -} - -._col1_3_sq96g_12 { - grid-column: 1/3; -} - -._col3_sq96g_16 { - grid-column: 3; -} - -._separator_sq96g_20 { - grid-column: -1/1; - height: 1px; - background-color: var(--bs-light-border-subtle); -} - -._padded_sq96g_26 { - margin-bottom: 1em; -} -._container_1ww70_1 { - margin: 1em 0 0 0; -} - -._titleRow_1ww70_5 { - display: flex; - align-items: center; - justify-content: space-between; -} - -._title_1ww70_5 { - font-weight: 600; - padding-bottom: 0.3em; -} -._wrapper_45f60_1 { - display: grid; - grid-template-columns: 0 auto auto; - column-gap: 1.5em; - row-gap: 0.2em; -} - -._col2_45f60_8 { - grid-column: 2; -} - -._col1_3_45f60_12 { - grid-column: 1/3; -} - -._col3_45f60_16 { - grid-column: 3; -} - -._separator_45f60_20 { - grid-column: -1/1; - height: 1px; - background-color: var(--bs-light-border-subtle); -} - -._topMargin_45f60_26 { - margin-top: 1em; -} -._container_e0l2n_1 { - margin: 0.5em 0 0 0; - width: 100%; -} - -._all_e0l2n_6 { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - column-gap: 1em; -} - -._tableSelection_e0l2n_12 { - width: fit-content; - align-self: start; - justify-self: start; -} - -._tools_e0l2n_18 { - grid-column: -1/1; -} - -._codePre_e0l2n_22 { - background: var(--bs-light); - width: 100%; - padding: 0.5em; - border-radius: var(--bs-border-radius); -} - -._code_e0l2n_22 { - white-space: pre-wrap !important; - word-wrap: anywhere !important; -} - -._progress_e0l2n_34 { - margin-left: 0.5em; -} - -._toolConfig_e0l2n_38 { - display: grid; - grid-template-columns: max-content auto; - column-gap: 1em; - row-gap: 0.5em; - padding-top: 0.5em; -} - -._toolChoice_e0l2n_46 { - border-top: solid var(--bs-light-border-subtle) 1px; - display: grid; - grid-template-columns: max-content auto; - column-gap: 1em; - margin-top: 0.5em; - padding-top: 0.5em; -} -._noMargin_1a3fk_1 { - margin-bottom: 0; -} - -._code_1a3fk_5 { - background: var(--bs-light); - border-radius: var(--bs-border-radius); -} - -._sample_1a3fk_10 { - margin: 1em 0em; -} - -._section_1a3fk_14 { - display: flex; - flex-wrap: wrap; - gap: 1em; - overflow-wrap: break-word; -} - -._metadata_1a3fk_21 { - margin: 0.5em 0; -} -._contents_1irga_1 { - margin-top: 0.5em; -} - -._contents_1irga_1 > :last-child { - margin-bottom: 0; -} - -._twoColumn_1irga_9 { - display: grid; - grid-template-columns: auto 1fr; - column-gap: 1.5em; -} - -._exec_1irga_15 { - margin-top: 0; -} - -._result_1irga_19 { - margin-top: 0.5em; -} - -._fileLabel_1irga_23 { - margin-top: 0; - margin-bottom: 0; -} - -._wrapPre_1irga_28 { - white-space: pre-wrap; - word-wrap: break-word; - overflow-wrap: break-word; - margin-bottom: 0; -} -._container_io1r0_1 { - display: grid; - grid-template-columns: max-content minmax(0, 1fr); - column-gap: 1em; - margin: 0; -} - -._wrappingContent_io1r0_8 { - word-break: break-word; - overflow-wrap: anywhere; -} - -._separator_io1r0_13 { - grid-column: 1 / -1; - border-bottom: solid 1px var(--bs-light-border-subtle); -} - -._metadata_io1r0_18 { - margin: 0.5em 0; -} - -._unchanged_io1r0_22 { - margin-top: 0.2em; - margin-bottom: 0; -} - -._section_io1r0_27 { - font-weight: 600; -} - -._spacer_io1r0_31 { - height: 2em; -} - -._section_io1r0_27 { - margin-top: 1em; -} -._explanation_1k2k0_1 { - display: grid; - grid-template-columns: max-content minmax(0, 1fr); - column-gap: 1em; - margin: 0.5em 0; -} - -._wrappingContent_1k2k0_8 { - word-break: break-word; - overflow-wrap: anywhere; -} - -._separator_1k2k0_13 { - grid-column: 1 / -1; - border-bottom: solid 1px var(--bs-light-border-subtle); -} - -._metadata_1k2k0_18 { - margin: 0.5em 0; -} -.ap-default-term-ff { - --term-font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace, "Symbols Nerd Font"; -} -div.ap-wrapper { - outline: none; - height: 100%; - display: flex; - justify-content: center; -} -div.ap-wrapper .title-bar { - display: none; - top: -78px; - transition: top 0.15s linear; - position: absolute; - left: 0; - right: 0; - box-sizing: content-box; - font-size: 20px; - line-height: 1em; - padding: 15px; - font-family: sans-serif; - color: white; - background-color: rgba(0, 0, 0, 0.8); -} -div.ap-wrapper .title-bar img { - vertical-align: middle; - height: 48px; - margin-right: 16px; -} -div.ap-wrapper .title-bar a { - color: white; - text-decoration: underline; -} -div.ap-wrapper .title-bar a:hover { - text-decoration: none; -} -div.ap-wrapper:fullscreen { - background-color: #000; - width: 100%; - align-items: center; -} -div.ap-wrapper:fullscreen .title-bar { - display: initial; -} -div.ap-wrapper:fullscreen.hud .title-bar { - top: 0; -} -div.ap-wrapper div.ap-player { - text-align: left; - display: inline-block; - padding: 0px; - position: relative; - box-sizing: content-box; - overflow: hidden; - max-width: 100%; - border-radius: 4px; - font-size: 15px; - background-color: var(--term-color-background); -} -.ap-player { - --term-color-foreground: #ffffff; - --term-color-background: #000000; - --term-color-0: var(--term-color-foreground); - --term-color-1: var(--term-color-foreground); - --term-color-2: var(--term-color-foreground); - --term-color-3: var(--term-color-foreground); - --term-color-4: var(--term-color-foreground); - --term-color-5: var(--term-color-foreground); - --term-color-6: var(--term-color-foreground); - --term-color-7: var(--term-color-foreground); - --term-color-8: var(--term-color-0); - --term-color-9: var(--term-color-1); - --term-color-10: var(--term-color-2); - --term-color-11: var(--term-color-3); - --term-color-12: var(--term-color-4); - --term-color-13: var(--term-color-5); - --term-color-14: var(--term-color-6); - --term-color-15: var(--term-color-7); -} -div.ap-term { - position: relative; - font-family: var(--term-font-family); - border-width: 0.75em; - border-radius: 0; - border-style: solid; - border-color: var(--term-color-background); - box-sizing: content-box; -} -.ap-term .ap-term-bg { - position: absolute; - inset: 0; -} -.ap-term .ap-term-text { - position: absolute; - inset: 0; - box-sizing: content-box; - overflow: hidden; - padding: 0; - margin: 0px; - display: block; - white-space: pre; - word-wrap: normal; - word-break: normal; - cursor: text; - color: var(--term-color-foreground); - outline: none; - line-height: var(--term-line-height); - font-family: inherit; - font-size: inherit; - font-variant-ligatures: none; - border: 0; - border-radius: 0; - background-color: transparent; -} -.ap-term-text .ap-line { - display: block; - width: 100%; - height: var(--term-line-height); - position: absolute; - top: calc(100% * var(--row) / var(--term-rows)); - letter-spacing: normal; - overflow: hidden; -} -.ap-term-text .ap-line span { - position: absolute; - left: calc(100% * var(--offset) / var(--term-cols)); - color: var(--fg); - padding: 0; - display: inline-block; - height: 100%; -} -.ap-term-text .ap-line .ap-inverse { - color: var(--bg); - background-color: var(--fg); -} -.ap-term-text .ap-line .ap-symbol { - text-align: center; -} -.ap-term-text .ap-line .cp-2580 { - border-top: calc(0.5 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2581 { - border-bottom: calc(0.125 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2582 { - border-bottom: calc(0.25 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2583 { - border-bottom: calc(0.375 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2584 { - border-bottom: calc(0.5 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2585 { - border-bottom: calc(0.625 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2586 { - border-bottom: calc(0.75 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2587 { - border-bottom: calc(0.875 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2588 { - background-color: var(--fg); -} -.ap-term-text .ap-line .cp-2589 { - border-left: 0.875ch solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-258a { - border-left: 0.75ch solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-258b { - border-left: 0.625ch solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-258c { - border-left: 0.5ch solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-258d { - border-left: 0.375ch solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-258e { - border-left: 0.25ch solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-258f { - border-left: 0.125ch solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2590 { - border-right: 0.5ch solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2591 { - background-color: color-mix(in srgb, var(--fg) 25%, var(--bg)); -} -.ap-term-text .ap-line .cp-2592 { - background-color: color-mix(in srgb, var(--fg) 50%, var(--bg)); -} -.ap-term-text .ap-line .cp-2593 { - background-color: color-mix(in srgb, var(--fg) 75%, var(--bg)); -} -.ap-term-text .ap-line .cp-2594 { - border-top: calc(0.125 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2595 { - border-right: 0.125ch solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2596 { - border-right: 0.5ch solid var(--bg); - border-top: calc(0.5 * var(--term-line-height)) solid var(--bg); - background-color: var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2597 { - border-left: 0.5ch solid var(--bg); - border-top: calc(0.5 * var(--term-line-height)) solid var(--bg); - background-color: var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2598 { - border-right: 0.5ch solid var(--bg); - border-bottom: calc(0.5 * var(--term-line-height)) solid var(--bg); - background-color: var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-2599 { - border-left: 0.5ch solid var(--fg); - border-bottom: calc(0.5 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-259a { - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-259a::before, -.ap-term-text .ap-line .cp-259a::after { - content: ''; - position: absolute; - width: 0.5ch; - height: calc(0.5 * var(--term-line-height)); - background-color: var(--fg); -} -.ap-term-text .ap-line .cp-259a::before { - top: 0; - left: 0; -} -.ap-term-text .ap-line .cp-259a::after { - bottom: 0; - right: 0; -} -.ap-term-text .ap-line .cp-259b { - border-left: 0.5ch solid var(--fg); - border-top: calc(0.5 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-259c { - border-right: 0.5ch solid var(--fg); - border-top: calc(0.5 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-259d { - border-left: 0.5ch solid var(--bg); - border-bottom: calc(0.5 * var(--term-line-height)) solid var(--bg); - background-color: var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-259e { - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-259e::before, -.ap-term-text .ap-line .cp-259e::after { - content: ''; - position: absolute; - width: 0.5ch; - height: calc(0.5 * var(--term-line-height)); - background-color: var(--fg); -} -.ap-term-text .ap-line .cp-259e::before { - top: 0; - right: 0; -} -.ap-term-text .ap-line .cp-259e::after { - bottom: 0; - left: 0; -} -.ap-term-text .ap-line .cp-259f { - border-right: 0.5ch solid var(--fg); - border-bottom: calc(0.5 * var(--term-line-height)) solid var(--fg); - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-e0b0 { - border-left: 1ch solid var(--fg); - border-top: calc(0.5 * var(--term-line-height)) solid transparent; - border-bottom: calc(0.5 * var(--term-line-height)) solid transparent; - box-sizing: border-box; - left: calc(-0.1px + 100% * var(--offset) / var(--term-cols)); -} -.ap-term-text .ap-line .cp-e0b1 { - color: var(--fg); -} -.ap-term-text .ap-line .cp-e0b1::after { - content: ""; - position: absolute; - inset: 0; - background-color: currentColor; - mask: no-repeat center / 100% 100% url('data:image/svg+xml;utf8,\ -\ - \ -'); - pointer-events: none; -} -.ap-term-text .ap-line .cp-e0b2 { - border-right: 1ch solid var(--fg); - border-top: calc(0.5 * var(--term-line-height)) solid transparent; - border-bottom: calc(0.5 * var(--term-line-height)) solid transparent; - box-sizing: border-box; -} -.ap-term-text .ap-line .cp-e0b3 { - color: var(--fg); -} -.ap-term-text .ap-line .cp-e0b3::after { - content: ""; - position: absolute; - inset: 0; - background-color: currentColor; - mask: no-repeat center / 100% 100% url('data:image/svg+xml;utf8,\ -\ - \ -'); - pointer-events: none; -} -.ap-term-text.ap-cursor-on .ap-line .ap-cursor { - color: var(--bg); - background-color: var(--fg); - border-radius: 0.05em; -} -.ap-term-text.ap-cursor-on .ap-line .ap-cursor.ap-inverse { - color: var(--fg); - background-color: var(--bg); -} -.ap-term-text:not(.ap-blink) .ap-line .ap-blink { - color: transparent; - border-color: transparent; -} -.ap-term-text .ap-bright { - font-weight: bold; -} -.ap-term-text .ap-faint { - opacity: 0.5; -} -.ap-term-text .ap-underline { - text-decoration: underline; -} -.ap-term-text .ap-italic { - font-style: italic; -} -.ap-term-text .ap-strikethrough { - text-decoration: line-through; -} -.ap-line span { - --fg: var(--term-color-foreground); - --bg: var(--term-color-background); -} -div.ap-player div.ap-control-bar { - width: 100%; - height: 32px; - display: flex; - justify-content: space-between; - align-items: stretch; - color: var(--term-color-foreground); - box-sizing: content-box; - line-height: 1; - position: absolute; - bottom: 0; - left: 0; - opacity: 0; - transition: opacity 0.15s linear; - user-select: none; - border-top: 2px solid color-mix(in oklab, var(--term-color-background) 80%, var(--term-color-foreground)); - z-index: 30; -} -div.ap-player div.ap-control-bar * { - box-sizing: inherit; -} -div.ap-control-bar svg.ap-icon path { - fill: var(--term-color-foreground); -} -div.ap-control-bar span.ap-button { - display: flex; - flex: 0 0 auto; - cursor: pointer; -} -div.ap-control-bar span.ap-playback-button { - width: 12px; - height: 12px; - padding: 10px; - margin: 0 0 0 2px; -} -div.ap-control-bar span.ap-playback-button svg { - height: 12px; - width: 12px; -} -div.ap-control-bar span.ap-timer { - display: flex; - flex: 0 0 auto; - min-width: 50px; - margin: 0 10px; - height: 100%; - text-align: center; - font-size: 13px; - line-height: 100%; - cursor: default; -} -div.ap-control-bar span.ap-timer span { - font-family: var(--term-font-family); - font-size: inherit; - font-weight: 600; - margin: auto; -} -div.ap-control-bar span.ap-timer .ap-time-remaining { - display: none; -} -div.ap-control-bar span.ap-timer:hover .ap-time-elapsed { - display: none; -} -div.ap-control-bar span.ap-timer:hover .ap-time-remaining { - display: flex; -} -div.ap-control-bar .ap-progressbar { - display: block; - flex: 1 1 auto; - height: 100%; - padding: 0 10px; -} -div.ap-control-bar .ap-progressbar .ap-bar { - display: block; - position: relative; - cursor: default; - height: 100%; - font-size: 0; -} -div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter { - display: block; - position: absolute; - top: 15px; - left: 0; - right: 0; - height: 3px; -} -div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter-empty { - background-color: color-mix(in oklab, var(--term-color-foreground) 20%, var(--term-color-background)); -} -div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter-full { - width: 100%; - transform-origin: left center; - background-color: var(--term-color-foreground); - border-radius: 3px; -} -div.ap-control-bar.ap-seekable .ap-progressbar .ap-bar { - cursor: pointer; -} -div.ap-control-bar .ap-fullscreen-button { - width: 14px; - height: 14px; - padding: 9px; - margin: 0 2px 0 4px; -} -div.ap-control-bar .ap-fullscreen-button svg { - width: 14px; - height: 14px; -} -div.ap-control-bar .ap-fullscreen-button svg.ap-icon-fullscreen-on { - display: inline; -} -div.ap-control-bar .ap-fullscreen-button svg.ap-icon-fullscreen-off { - display: none; -} -div.ap-control-bar .ap-fullscreen-button .ap-tooltip { - right: 5px; - left: initial; - transform: none; -} -div.ap-control-bar .ap-kbd-button { - height: 14px; - padding: 9px; - margin: 0 0 0 4px; -} -div.ap-control-bar .ap-kbd-button svg { - width: 26px; - height: 14px; -} -div.ap-control-bar .ap-kbd-button .ap-tooltip { - right: 5px; - left: initial; - transform: none; -} -div.ap-control-bar .ap-speaker-button { - width: 19px; - padding: 6px 9px; - margin: 0 0 0 4px; - position: relative; -} -div.ap-control-bar .ap-speaker-button svg { - width: 19px; -} -div.ap-control-bar .ap-speaker-button .ap-tooltip { - left: -50%; - transform: none; -} -div.ap-wrapper.ap-hud .ap-control-bar { - opacity: 1; -} -div.ap-wrapper:fullscreen .ap-fullscreen-button svg.ap-icon-fullscreen-on { - display: none; -} -div.ap-wrapper:fullscreen .ap-fullscreen-button svg.ap-icon-fullscreen-off { - display: inline; -} -span.ap-progressbar span.ap-marker-container { - display: block; - top: 0; - bottom: 0; - width: 21px; - position: absolute; - margin-left: -10px; -} -span.ap-marker-container span.ap-marker { - display: block; - top: 13px; - bottom: 12px; - left: 7px; - right: 7px; - background-color: color-mix(in oklab, var(--term-color-foreground) 33%, var(--term-color-background)); - position: absolute; - transition: top 0.1s, bottom 0.1s, left 0.1s, right 0.1s, background-color 0.1s; - border-radius: 50%; -} -span.ap-marker-container span.ap-marker.ap-marker-past { - background-color: var(--term-color-foreground); -} -span.ap-marker-container span.ap-marker:hover, -span.ap-marker-container:hover span.ap-marker { - background-color: var(--term-color-foreground); - top: 11px; - bottom: 10px; - left: 5px; - right: 5px; -} -.ap-tooltip-container span.ap-tooltip { - visibility: hidden; - background-color: var(--term-color-foreground); - color: var(--term-color-background); - font-family: var(--term-font-family); - font-weight: bold; - text-align: center; - padding: 0 0.5em; - border-radius: 4px; - position: absolute; - z-index: 1; - white-space: nowrap; - /* Prevents the text from wrapping and makes sure the tooltip width adapts to the text length */ - font-size: 13px; - line-height: 2em; - bottom: 100%; - left: 50%; - transform: translateX(-50%); -} -.ap-tooltip-container:hover span.ap-tooltip { - visibility: visible; -} -.ap-player .ap-overlay { - z-index: 10; - background-repeat: no-repeat; - background-position: center; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; -} -.ap-player .ap-overlay-start { - cursor: pointer; -} -.ap-player .ap-overlay-start .ap-play-button { - font-size: 0px; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - text-align: center; - color: white; - height: 80px; - max-height: 66%; - margin: auto; -} -.ap-player .ap-overlay-start .ap-play-button div { - height: 100%; -} -.ap-player .ap-overlay-start .ap-play-button div span { - height: 100%; - display: block; -} -.ap-player .ap-overlay-start .ap-play-button div span svg { - height: 100%; - display: inline-block; -} -.ap-player .ap-overlay-start .ap-play-button svg { - filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.4)); -} -.ap-player .ap-overlay-loading .ap-loader { - width: 48px; - height: 48px; - border-radius: 50%; - display: inline-block; - position: relative; - border: 10px solid; - border-color: rgba(255, 255, 255, 0.3) rgba(255, 255, 255, 0.5) rgba(255, 255, 255, 0.7) #ffffff; - border-color: color-mix(in srgb, var(--term-color-foreground) 30%, var(--term-color-background)) color-mix(in srgb, var(--term-color-foreground) 50%, var(--term-color-background)) color-mix(in srgb, var(--term-color-foreground) 70%, var(--term-color-background)) color-mix(in srgb, var(--term-color-foreground) 100%, var(--term-color-background)); - box-sizing: border-box; - animation: ap-loader-rotation 1s linear infinite; -} -.ap-player .ap-overlay-info { - background-color: var(--term-color-background); -} -.ap-player .ap-overlay-info span { - font-family: var(--term-font-family); - font-size: 2em; - color: var(--term-color-foreground); -} -.ap-player .ap-overlay-help { - background-color: rgba(0, 0, 0, 0.8); - container-type: inline-size; -} -.ap-player .ap-overlay-help > div { - font-family: var(--term-font-family); - max-width: 85%; - max-height: 85%; - font-size: 18px; - color: var(--term-color-foreground); - box-sizing: border-box; - margin-bottom: 32px; -} -.ap-player .ap-overlay-help > div div { - padding: calc(min(4cqw, 40px)); - font-size: calc(min(1.9cqw, 18px)); - background-color: var(--term-color-background); - border: 1px solid color-mix(in oklab, var(--term-color-background) 90%, var(--term-color-foreground)); - border-radius: 6px; -} -.ap-player .ap-overlay-help > div div p { - font-weight: bold; - margin: 0 0 2em 0; -} -.ap-player .ap-overlay-help > div div ul { - list-style: none; - padding: 0; -} -.ap-player .ap-overlay-help > div div ul li { - margin: 0 0 0.75em 0; -} -.ap-player .ap-overlay-help > div div kbd { - color: var(--term-color-background); - background-color: var(--term-color-foreground); - padding: 0.2em 0.5em; - border-radius: 0.2em; - font-family: inherit; - font-size: 0.85em; - border: none; - margin: 0; -} -.ap-player .ap-overlay-error span { - font-size: 8em; -} -.ap-player .slide-enter-active { - transition: opacity 0.2s; -} -.ap-player .slide-enter-active.ap-was-playing { - transition: top 0.2s ease-out, opacity 0.2s; -} -.ap-player .slide-exit-active { - transition: top 0.2s ease-in, opacity 0.2s; -} -.ap-player .slide-enter { - top: -50%; - opacity: 0; -} -.ap-player .slide-enter-to { - top: 0%; -} -.ap-player .slide-enter, -.ap-player .slide-enter-to, -.ap-player .slide-exit, -.ap-player .slide-exit-to { - bottom: auto; - height: 100%; -} -.ap-player .slide-exit { - top: 0%; -} -.ap-player .slide-exit-to { - top: -50%; - opacity: 0; -} -@keyframes ap-loader-rotation { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} -.ap-term { - --term-color-16: #000000; - --term-color-17: #00005f; - --term-color-18: #000087; - --term-color-19: #0000af; - --term-color-20: #0000d7; - --term-color-21: #0000ff; - --term-color-22: #005f00; - --term-color-23: #005f5f; - --term-color-24: #005f87; - --term-color-25: #005faf; - --term-color-26: #005fd7; - --term-color-27: #005fff; - --term-color-28: #008700; - --term-color-29: #00875f; - --term-color-30: #008787; - --term-color-31: #0087af; - --term-color-32: #0087d7; - --term-color-33: #0087ff; - --term-color-34: #00af00; - --term-color-35: #00af5f; - --term-color-36: #00af87; - --term-color-37: #00afaf; - --term-color-38: #00afd7; - --term-color-39: #00afff; - --term-color-40: #00d700; - --term-color-41: #00d75f; - --term-color-42: #00d787; - --term-color-43: #00d7af; - --term-color-44: #00d7d7; - --term-color-45: #00d7ff; - --term-color-46: #00ff00; - --term-color-47: #00ff5f; - --term-color-48: #00ff87; - --term-color-49: #00ffaf; - --term-color-50: #00ffd7; - --term-color-51: #00ffff; - --term-color-52: #5f0000; - --term-color-53: #5f005f; - --term-color-54: #5f0087; - --term-color-55: #5f00af; - --term-color-56: #5f00d7; - --term-color-57: #5f00ff; - --term-color-58: #5f5f00; - --term-color-59: #5f5f5f; - --term-color-60: #5f5f87; - --term-color-61: #5f5faf; - --term-color-62: #5f5fd7; - --term-color-63: #5f5fff; - --term-color-64: #5f8700; - --term-color-65: #5f875f; - --term-color-66: #5f8787; - --term-color-67: #5f87af; - --term-color-68: #5f87d7; - --term-color-69: #5f87ff; - --term-color-70: #5faf00; - --term-color-71: #5faf5f; - --term-color-72: #5faf87; - --term-color-73: #5fafaf; - --term-color-74: #5fafd7; - --term-color-75: #5fafff; - --term-color-76: #5fd700; - --term-color-77: #5fd75f; - --term-color-78: #5fd787; - --term-color-79: #5fd7af; - --term-color-80: #5fd7d7; - --term-color-81: #5fd7ff; - --term-color-82: #5fff00; - --term-color-83: #5fff5f; - --term-color-84: #5fff87; - --term-color-85: #5fffaf; - --term-color-86: #5fffd7; - --term-color-87: #5fffff; - --term-color-88: #870000; - --term-color-89: #87005f; - --term-color-90: #870087; - --term-color-91: #8700af; - --term-color-92: #8700d7; - --term-color-93: #8700ff; - --term-color-94: #875f00; - --term-color-95: #875f5f; - --term-color-96: #875f87; - --term-color-97: #875faf; - --term-color-98: #875fd7; - --term-color-99: #875fff; - --term-color-100: #878700; - --term-color-101: #87875f; - --term-color-102: #878787; - --term-color-103: #8787af; - --term-color-104: #8787d7; - --term-color-105: #8787ff; - --term-color-106: #87af00; - --term-color-107: #87af5f; - --term-color-108: #87af87; - --term-color-109: #87afaf; - --term-color-110: #87afd7; - --term-color-111: #87afff; - --term-color-112: #87d700; - --term-color-113: #87d75f; - --term-color-114: #87d787; - --term-color-115: #87d7af; - --term-color-116: #87d7d7; - --term-color-117: #87d7ff; - --term-color-118: #87ff00; - --term-color-119: #87ff5f; - --term-color-120: #87ff87; - --term-color-121: #87ffaf; - --term-color-122: #87ffd7; - --term-color-123: #87ffff; - --term-color-124: #af0000; - --term-color-125: #af005f; - --term-color-126: #af0087; - --term-color-127: #af00af; - --term-color-128: #af00d7; - --term-color-129: #af00ff; - --term-color-130: #af5f00; - --term-color-131: #af5f5f; - --term-color-132: #af5f87; - --term-color-133: #af5faf; - --term-color-134: #af5fd7; - --term-color-135: #af5fff; - --term-color-136: #af8700; - --term-color-137: #af875f; - --term-color-138: #af8787; - --term-color-139: #af87af; - --term-color-140: #af87d7; - --term-color-141: #af87ff; - --term-color-142: #afaf00; - --term-color-143: #afaf5f; - --term-color-144: #afaf87; - --term-color-145: #afafaf; - --term-color-146: #afafd7; - --term-color-147: #afafff; - --term-color-148: #afd700; - --term-color-149: #afd75f; - --term-color-150: #afd787; - --term-color-151: #afd7af; - --term-color-152: #afd7d7; - --term-color-153: #afd7ff; - --term-color-154: #afff00; - --term-color-155: #afff5f; - --term-color-156: #afff87; - --term-color-157: #afffaf; - --term-color-158: #afffd7; - --term-color-159: #afffff; - --term-color-160: #d70000; - --term-color-161: #d7005f; - --term-color-162: #d70087; - --term-color-163: #d700af; - --term-color-164: #d700d7; - --term-color-165: #d700ff; - --term-color-166: #d75f00; - --term-color-167: #d75f5f; - --term-color-168: #d75f87; - --term-color-169: #d75faf; - --term-color-170: #d75fd7; - --term-color-171: #d75fff; - --term-color-172: #d78700; - --term-color-173: #d7875f; - --term-color-174: #d78787; - --term-color-175: #d787af; - --term-color-176: #d787d7; - --term-color-177: #d787ff; - --term-color-178: #d7af00; - --term-color-179: #d7af5f; - --term-color-180: #d7af87; - --term-color-181: #d7afaf; - --term-color-182: #d7afd7; - --term-color-183: #d7afff; - --term-color-184: #d7d700; - --term-color-185: #d7d75f; - --term-color-186: #d7d787; - --term-color-187: #d7d7af; - --term-color-188: #d7d7d7; - --term-color-189: #d7d7ff; - --term-color-190: #d7ff00; - --term-color-191: #d7ff5f; - --term-color-192: #d7ff87; - --term-color-193: #d7ffaf; - --term-color-194: #d7ffd7; - --term-color-195: #d7ffff; - --term-color-196: #ff0000; - --term-color-197: #ff005f; - --term-color-198: #ff0087; - --term-color-199: #ff00af; - --term-color-200: #ff00d7; - --term-color-201: #ff00ff; - --term-color-202: #ff5f00; - --term-color-203: #ff5f5f; - --term-color-204: #ff5f87; - --term-color-205: #ff5faf; - --term-color-206: #ff5fd7; - --term-color-207: #ff5fff; - --term-color-208: #ff8700; - --term-color-209: #ff875f; - --term-color-210: #ff8787; - --term-color-211: #ff87af; - --term-color-212: #ff87d7; - --term-color-213: #ff87ff; - --term-color-214: #ffaf00; - --term-color-215: #ffaf5f; - --term-color-216: #ffaf87; - --term-color-217: #ffafaf; - --term-color-218: #ffafd7; - --term-color-219: #ffafff; - --term-color-220: #ffd700; - --term-color-221: #ffd75f; - --term-color-222: #ffd787; - --term-color-223: #ffd7af; - --term-color-224: #ffd7d7; - --term-color-225: #ffd7ff; - --term-color-226: #ffff00; - --term-color-227: #ffff5f; - --term-color-228: #ffff87; - --term-color-229: #ffffaf; - --term-color-230: #ffffd7; - --term-color-231: #ffffff; - --term-color-232: #080808; - --term-color-233: #121212; - --term-color-234: #1c1c1c; - --term-color-235: #262626; - --term-color-236: #303030; - --term-color-237: #3a3a3a; - --term-color-238: #444444; - --term-color-239: #4e4e4e; - --term-color-240: #585858; - --term-color-241: #626262; - --term-color-242: #6c6c6c; - --term-color-243: #767676; - --term-color-244: #808080; - --term-color-245: #8a8a8a; - --term-color-246: #949494; - --term-color-247: #9e9e9e; - --term-color-248: #a8a8a8; - --term-color-249: #b2b2b2; - --term-color-250: #bcbcbc; - --term-color-251: #c6c6c6; - --term-color-252: #d0d0d0; - --term-color-253: #dadada; - --term-color-254: #e4e4e4; - --term-color-255: #eeeeee; -} -.asciinema-player-theme-asciinema { - --term-color-foreground: #cccccc; - --term-color-background: #121314; - --term-color-0: hsl(0, 0%, 0%); - --term-color-1: hsl(343, 70%, 55%); - --term-color-2: hsl(103, 70%, 44%); - --term-color-3: hsl(43, 70%, 55%); - --term-color-4: hsl(193, 70%, 49.5%); - --term-color-5: hsl(283, 70%, 60.5%); - --term-color-6: hsl(163, 70%, 60.5%); - --term-color-7: hsl(0, 0%, 85%); - --term-color-8: hsl(0, 0%, 30%); - --term-color-9: hsl(343, 70%, 55%); - --term-color-10: hsl(103, 70%, 44%); - --term-color-11: hsl(43, 70%, 55%); - --term-color-12: hsl(193, 70%, 49.5%); - --term-color-13: hsl(283, 70%, 60.5%); - --term-color-14: hsl(163, 70%, 60.5%); - --term-color-15: hsl(0, 0%, 100%); -} -/* - Based on Dracula: https://draculatheme.com - */ -.asciinema-player-theme-dracula { - --term-color-foreground: #f8f8f2; - --term-color-background: #282a36; - --term-color-0: #21222c; - --term-color-1: #ff5555; - --term-color-2: #50fa7b; - --term-color-3: #f1fa8c; - --term-color-4: #bd93f9; - --term-color-5: #ff79c6; - --term-color-6: #8be9fd; - --term-color-7: #f8f8f2; - --term-color-8: #6272a4; - --term-color-9: #ff6e6e; - --term-color-10: #69ff94; - --term-color-11: #ffffa5; - --term-color-12: #d6acff; - --term-color-13: #ff92df; - --term-color-14: #a4ffff; - --term-color-15: #ffffff; -} -/* Based on Monokai from base16 collection - https://github.com/chriskempson/base16 */ -.asciinema-player-theme-monokai { - --term-color-foreground: #f8f8f2; - --term-color-background: #272822; - --term-color-0: #272822; - --term-color-1: #f92672; - --term-color-2: #a6e22e; - --term-color-3: #f4bf75; - --term-color-4: #66d9ef; - --term-color-5: #ae81ff; - --term-color-6: #a1efe4; - --term-color-7: #f8f8f2; - --term-color-8: #75715e; - --term-color-15: #f9f8f5; -} -/* - Based on Nord: https://github.com/arcticicestudio/nord - Via: https://github.com/neilotoole/asciinema-theme-nord - */ -.asciinema-player-theme-nord { - --term-color-foreground: #eceff4; - --term-color-background: #2e3440; - --term-color-0: #3b4252; - --term-color-1: #bf616a; - --term-color-2: #a3be8c; - --term-color-3: #ebcb8b; - --term-color-4: #81a1c1; - --term-color-5: #b48ead; - --term-color-6: #88c0d0; - --term-color-7: #eceff4; -} -.asciinema-player-theme-seti { - --term-color-foreground: #cacecd; - --term-color-background: #111213; - --term-color-0: #323232; - --term-color-1: #c22832; - --term-color-2: #8ec43d; - --term-color-3: #e0c64f; - --term-color-4: #43a5d5; - --term-color-5: #8b57b5; - --term-color-6: #8ec43d; - --term-color-7: #eeeeee; - --term-color-15: #ffffff; -} -/* - Based on Solarized Dark: https://ethanschoonover.com/solarized/ - */ -.asciinema-player-theme-solarized-dark { - --term-color-foreground: #839496; - --term-color-background: #002b36; - --term-color-0: #073642; - --term-color-1: #dc322f; - --term-color-2: #859900; - --term-color-3: #b58900; - --term-color-4: #268bd2; - --term-color-5: #d33682; - --term-color-6: #2aa198; - --term-color-7: #eee8d5; - --term-color-8: #002b36; - --term-color-9: #cb4b16; - --term-color-10: #586e75; - --term-color-11: #657b83; - --term-color-12: #839496; - --term-color-13: #6c71c4; - --term-color-14: #93a1a1; - --term-color-15: #fdf6e3; -} -/* - Based on Solarized Light: https://ethanschoonover.com/solarized/ - */ -.asciinema-player-theme-solarized-light { - --term-color-foreground: #657b83; - --term-color-background: #fdf6e3; - --term-color-0: #073642; - --term-color-1: #dc322f; - --term-color-2: #859900; - --term-color-3: #b58900; - --term-color-4: #268bd2; - --term-color-5: #d33682; - --term-color-6: #2aa198; - --term-color-7: #eee8d5; - --term-color-8: #002b36; - --term-color-9: #cb4b16; - --term-color-10: #586e75; - --term-color-11: #657c83; - --term-color-12: #839496; - --term-color-13: #6c71c4; - --term-color-14: #93a1a1; - --term-color-15: #fdf6e3; -} -.asciinema-player-theme-solarized-light .ap-overlay-start .ap-play-button svg .ap-play-btn-fill { - fill: var(--term-color-1); -} -.asciinema-player-theme-solarized-light .ap-overlay-start .ap-play-button svg .ap-play-btn-stroke { - stroke: var(--term-color-1); -} -/* - Based on Tango: https://en.wikipedia.org/wiki/Tango_Desktop_Project - */ -.asciinema-player-theme-tango { - --term-color-foreground: #cccccc; - --term-color-background: #121314; - --term-color-0: #000000; - --term-color-1: #cc0000; - --term-color-2: #4e9a06; - --term-color-3: #c4a000; - --term-color-4: #3465a4; - --term-color-5: #75507b; - --term-color-6: #06989a; - --term-color-7: #d3d7cf; - --term-color-8: #555753; - --term-color-9: #ef2929; - --term-color-10: #8ae234; - --term-color-11: #fce94f; - --term-color-12: #729fcf; - --term-color-13: #ad7fa8; - --term-color-14: #34e2e2; - --term-color-15: #eeeeec; -} -/* - Based on gruvbox: https://github.com/morhetz/gruvbox - */ -.asciinema-player-theme-gruvbox-dark { - --term-color-foreground: #fbf1c7; - --term-color-background: #282828; - --term-color-0: #282828; - --term-color-1: #cc241d; - --term-color-2: #98971a; - --term-color-3: #d79921; - --term-color-4: #458588; - --term-color-5: #b16286; - --term-color-6: #689d6a; - --term-color-7: #a89984; - --term-color-8: #7c6f65; - --term-color-9: #fb4934; - --term-color-10: #b8bb26; - --term-color-11: #fabd2f; - --term-color-12: #83a598; - --term-color-13: #d3869b; - --term-color-14: #8ec07c; - --term-color-15: #fbf1c7; -} -.asciinema-player { - max-height: 100vh; - max-width: 100vw; -} - -.asciinema-player-status { - margin-right: 0.5em; -} - -.asciinema-wrapper { - display: flex; - justify-content: center; -} - -.asciinema-container { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - grid-template-rows: auto auto; - width: 100%; -} - -.asciinema-header-left { - justify-self: start; - font-size: var(--inspect-font-size-small); -} - -.asciinema-header-center { - justify-self: center; - font-size: var(--inspect-font-size-small); -} - -.asciinema-header-right { - justify-self: start; - font-size: var(--inspect-font-size-small); -} - -.asciinema-body { - border-top: 1px solid var(--bs-light-border-subtle); - grid-column: span 3; - width: 100%; -} -._carouselThumbs_1mvg8_1 { - display: grid; - grid-template-columns: auto auto auto auto; -} - -._carouselThumb_1mvg8_1 { - background: black; - color: white; - padding: 4em 0; - border: 0; - margin: 5px; - cursor: pointer; - text-align: center; -} - -._carouselPlayIcon_1mvg8_16 { - font-size: 4em; -} - -._lightboxOverlay_1mvg8_20 { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: rgba(0, 0, 0, 0.8); - display: flex; - align-items: center; - justify-content: center; - z-index: 9998; -} - -._lightboxContent_1mvg8_33._open_1mvg8_33, -._lightboxOverlay_1mvg8_20._open_1mvg8_33 { - opacity: 1; - visibility: visible; -} - -._lightboxContent_1mvg8_33._closed_1mvg8_39, -._lightboxOverlay_1mvg8_20._closed_1mvg8_39 { - opacity: 0; - visibility: hidden; -} - -._lightboxButtonCloseWrapper_1mvg8_45 { - position: absolute; - top: 10px; - right: 10px; -} - -._lightboxButtonClose_1mvg8_45 { - border: none; - background: none; - color: #fff; - font-size: 3em; - font-weight: 500; - cursor: pointer; - padding-left: 1em; - padding-bottom: 1em; - z-index: 10000; -} - -._lightboxPreviewButton_1mvg8_63 { - position: absolute; - top: 50%; - transform: translateY(-50%); - background: none; - color: #fff; - border: none; - padding: 0.5em; - font-size: 3em; - cursor: pointer; - z-index: 9999; -} - -._lightboxPreviewButton_1mvg8_63._next_1mvg8_76 { - left: 10px; -} - -._lightboxPreviewButton_1mvg8_63._prev_1mvg8_80 { - right: 10px; -} - -._lightboxContent_1mvg8_33 { - max-width: 90vw !important; - max-height: 90vh !important; - display: flex; - position: relative; - transition: - opacity 0.3s ease, - visibility 0.3s ease; - z-index: 9999; - overflow: hidden; -} - -._lightboxContent_1mvg8_33 > * { - max-height: 90vh !important; - max-width: 90vw !important; -} -._toolsGrid_1qqm2_1 { - display: grid; - row-gap: 0em; -} - -._tools_1qqm2_1 { - display: grid; - grid-template-columns: max-content max-content; - column-gap: 1rem; - margin: 0; - align-items: baseline; -} - -._tool_1qqm2_1 { - padding: 0; - color: inherit !important; -} -._diff_eobja_1 { - margin: 1em 0em; - width: 100%; -} - -._summary_eobja_6 { - margin: 1em 0em; - width: 100%; -} -._summary_ac4z2_1 { - width: 100%; - margin: 0.5em 0em; -} - -._summaryRendered_ac4z2_6 { - margin-bottom: 1em; -} - -._subtaskSummary_ac4z2_10 { - display: grid; - grid-template-columns: minmax(0, 1fr) max-content minmax(0, 1fr); - column-gap: 1em; - margin: 0.5em 0; -} - -._subtaskLabel_ac4z2_17 { - padding: 0 2em; -} -._summary_1qsnv_1 { - margin: 0.5em 0; - width: 100%; -} - -._approval_1qsnv_6 { - border: none; - padding: 0; - margin-bottom: 0; -} - -._progress_1qsnv_12 { - margin-left: 0.5em; -} -._node_engat_1 { - padding-top: 0.8rem; -} - -._attached_engat_5 { - padding-top: 0rem; -} - -._attachedParent_engat_9 { - padding-bottom: 0px; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - border-bottom: none; -} - -._attachedChild_engat_16 { - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -._last_engat_21 { - padding-bottom: 0.8rem; -} -._row_1ukc2_1 { - border-bottom: solid 1px var(--bs-border-color); - padding: 0.5rem 1rem 1rem 1rem; - max-height: 200px; -} - -._row_1ukc2_1._noExplanation_1ukc2_7 { - max-height: 100px; -} - -._row_1ukc2_1:hover { - cursor: pointer; -} - -._row_1ukc2_1._disabled_1ukc2_15 { - opacity: 0.7; -} - -._row_1ukc2_1._disabled_1ukc2_15:hover { - cursor: default; -} - -._result_1ukc2_23 { - display: grid; - grid-template-columns: max-content 1fr; - grid-template-rows: auto auto; - column-gap: 0.5rem; - row-gap: 0.5rem; - justify-content: space-between; -} - -._id_1ukc2_32 { -} - -._model_1ukc2_35 { - text-align: right; -} - -._label_1ukc2_39 { - overflow-wrap: break-word; -} - -._explanation_1ukc2_43 { - overflow: hidden; - text-overflow: ellipsis; - - grid-column: span 2; - max-height: calc(160px - 3rem); -} - -._row_1ukc2_1._selected_1ukc2_51 { - background-color: rgba(var(--bs-secondary-rgb), 0.07); -} - -._link_1ukc2_55 { - color: var(--bs-body-color); - text-decoration: none; -} - -._link_1ukc2_55:hover { - color: var(--bs-body-color); -} - -._error_1ukc2_64 { - overflow: hidden; - text-overflow: ellipsis; - max-height: calc(160px - 3rem); -} -._scannerHeaderRow_94id2_1 { - display: grid; - grid-template-columns: auto auto; - column-gap: 0.5em; - margin-bottom: 1em; -} - -._controls_94id2_8 { - display: flex; - align-items: flex-end; - justify-content: flex-end; - margin-bottom: 0.5em; -} - -._scrollContainer_94id2_15 { - overflow-y: auto; -} -._root_1nj72_1 { - display: grid; - grid-template-rows: max-content 1fr max-content; - height: 100%; - width: 100%; -} - -._container_1nj72_8 { - display: grid; - grid-template-columns: 200px 1fr; - height: 100%; - width: 100%; -} - -._breadcrumbs_1nj72_15 { - min-width: 600px !important; -} -._container_10aby_1 { - border-right: solid 1px var(--bs-border-color); -} - -._entry_10aby_5 { - padding: 0.5em; - display: grid; - grid-template-columns: auto 1fr; - column-gap: 0.5rem; -} - -._entry_10aby_5._selected_10aby_12 { - background-color: var(--bs-secondary-bg); -} - -._titleBlock_10aby_16 { - margin-bottom: 0.1rem; - grid-column: 1 / -1; -} - -._validations_10aby_21 { - margin-top: 0.3rem; - grid-column: 1 / -1; -} - -._subTitle_10aby_26 { - margin-top: -0.2rem; - max-height: 2rem; - overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 2; /* adjust based on your line-height */ - -webkit-box-orient: vertical; - word-wrap: break-word; -} - -._selected_10aby_12 ._title_10aby_16 { - font-weight: 600; -} - -._entry_10aby_5:hover { - color: var(--bs-link-hover-color); - cursor: pointer; -} - -._numericResultTable_10aby_45 { - display: grid; - grid-template-columns: auto auto; - column-gap: 0.5rem; - row-gap: 0; -} - -._contents_10aby_52 { - display: contents; -} -._tabSet_10t9t_1 { - overflow-y: hidden; -} - -._tabs_10t9t_5 { - padding: 0.5em !important; - border-bottom: solid 1px var(--bs-border-color); -} - -._tabControl_10t9t_10 { - padding: 0.3em 1em !important; -} -._scanTitleView_1o9vc_1 { - display: grid; - grid-template-columns: max-content max-content; - justify-content: space-between; - column-gap: 1em; - padding: 0.5em; -} - -._leftColumn_1o9vc_9 { - display: grid; - grid-template-columns: max-content max-content 1fr; - align-items: baseline; - row-gap: 0.2rem; - column-gap: 0.3rem; -} - -._rightColumn_1o9vc_17 { -} - -._scanTitleView_1o9vc_1 h1 { - margin: 0; - font-weight: 600; - font-size: 1.3em; -} - -._scanTitleView_1o9vc_1 h2 { - margin: 0; - font-weight: 500; - font-size: 1em; - color: var(--bs-secondary-text-color); -} - -._scanTitleView_1o9vc_1 ._secondaryRow_1o9vc_33 { - display: grid; - grid-template-columns: max-content max-content; - column-gap: 0.2rem; -} - -._subtitle_1o9vc_39 { - grid-column: 1 / -1; - display: grid; - grid-template-columns: max-content max-content max-content max-content max-content; - column-gap: 0.1rem; - margin: 0; - font-size: 0.8em; - font-weight: 400; - color: var(--bs-secondary-text-color); -} -._content_10rzs_1 { - display: flex; - flex-direction: column; - gap: 12px; -} - -._content_10rzs_1 p { - margin: 0; -} - -._warning_10rzs_11 { - color: var(--vscode-errorForeground); - font-size: 0.9rem; -} -._wrapper_poq0z_1 { - position: relative; - display: inline-block; -} - -._iconButton_poq0z_6 { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - padding: 0; - background: transparent; - border: none; - border-radius: 4px; - color: var(--vscode-descriptionForeground); - cursor: pointer; - transition: - background-color 0.1s, - color 0.1s; -} - -._iconButton_poq0z_6:hover { - background: var(--vscode-toolbar-hoverBackground); - color: var(--vscode-foreground); -} - -._iconButton_poq0z_6:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -._backdrop_poq0z_33 { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 1000; -} - -._menu_poq0z_42 { - position: absolute; - top: 100%; - right: 0; - margin-top: 2px; - background: var( - --vscode-menu-background, - var(--vscode-editorWidget-background) - ); - border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); - border-radius: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - z-index: 1001; - min-width: 160px; - overflow: hidden; - padding: 4px 0; -} - -._menuItem_poq0z_60 { - display: block; - width: 100%; - padding: 4px 12px; - background: none; - border: none; - text-align: left; - cursor: pointer; - color: var(--vscode-menu-foreground, var(--vscode-foreground)); - white-space: nowrap; - font-size: 13px; - line-height: 22px; -} - -._menuItem_poq0z_60:hover { - background: var( - --vscode-menu-selectionBackground, - var(--vscode-list-hoverBackground) - ); - color: var(--vscode-menu-selectionForeground, var(--vscode-foreground)); -} - -._menuItem_poq0z_60:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -._menuItem_poq0z_60 i { - margin-right: 8px; -} -._container_oam2j_1 { - display: flex; - flex-direction: column; - padding: 0; -} - -._header_oam2j_7 { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.5rem; - flex-shrink: 0; - height: 39px; - border-bottom: 1px solid var(--bs-border-color); - background-color: var(--bs-light); -} - -._headerTitle_oam2j_18 { - margin: 0; - font-size: var(--inspect-font-size-smallest); - color: var(--bs-body-color); - text-transform: uppercase; - font-weight: 400; -} - -._headerIcon_oam2j_26 { - margin-right: 0.5em; -} - -._headerSecondary_oam2j_30 { - font-size: var(--inspect-font-size-smallest); - color: var(--bs-secondary-color); -} - -._content_oam2j_35 { - flex: 1; - overflow-y: auto; -} - -._placeholder_oam2j_40 { - color: var(--bs-secondary-color); - font-size: 0.875rem; - text-align: center; - margin-top: 2rem; -} - -._textValue_oam2j_47 { - font-family: var(--bs-font-monospace); - font-size: var(--inspect-font-size-smaller); -} - -._panel_oam2j_52 { - background-color: var(--bs-body-bg); - display: flex; - flex-direction: column; - gap: 0.75rem; - padding: 0.5rem 0.5rem 1.5rem 0.5rem; -} - -/* Make all form controls full width within the panel */ -._panel_oam2j_52 vscode-single-select, -._panel_oam2j_52 vscode-textfield, -._panel_oam2j_52 button { - width: 100% !important; - min-width: 0 !important; - max-width: 100% !important; -} - -._clickable_oam2j_69 { - cursor: pointer; -} - -._idLabel_oam2j_73 { - margin-right: 0.5em; -} - -._saveStatusContainer_oam2j_77 { - position: fixed; - bottom: 0.1rem; - right: 0.1rem; - padding: 0.25rem 0.5rem; - background-color: var(--bs-body-bg); - border-radius: var(--bs-border-radius); - max-width: 175px; - line-height: 1.3; - pointer-events: none; - transition: opacity 300ms ease-in-out; -} - -._saveStatusContainer_oam2j_77._saveStatusHidden_oam2j_90 { - opacity: 0; -} - -._saveStatusContainer_oam2j_77._saveStatusError_oam2j_94 { - opacity: 1; - color: var(--bs-danger); -} - -._saveStatus_oam2j_77 { - font-size: var(--inspect-font-size-smallest); - color: var(--bs-secondary-color); - word-wrap: break-word; - overflow-wrap: break-word; - display: block; -} - -._createError_oam2j_107 { - color: var(--vscode-errorForeground); - font-size: var(--inspect-font-size-smallest); - margin-top: 0.25rem; -} - -._headerActionButton_oam2j_113 { - display: flex; - align-items: center; - justify-content: center; - width: 12px; - height: 12px; - padding: 5px; - background: none; - border: none; - border-radius: 4px; - color: var(--vscode-descriptionForeground); - cursor: pointer; - opacity: 0.6; - font-size: 12px; - pointer-events: auto; - position: relative; - z-index: 10; -} - -._headerActionButton_oam2j_113:hover:not(:disabled) { - opacity: 1; - background: var(--vscode-toolbar-hoverBackground); - color: var(--vscode-errorForeground); -} - -._headerActionButton_oam2j_113:disabled { - opacity: 0.3; - cursor: default; -} -._infoBox_oam2j_142 { - display: grid; - grid-template-columns: auto 1fr; - column-gap: 0.3rem; -} - -._idField_oam2j_148 { - display: grid; - grid-template-columns: auto 1fr; -} - -._idValue_oam2j_153 { - overflow-wrap: break-word; - word-break: break-all; -} -._inputContainer_1nqcj_1 { - display: flex; - flex-wrap: wrap; - gap: 0.3rem; - padding: 4px 6px; - min-height: 30px; - align-items: center; - align-content: flex-start; - background-color: var(--vscode-input-background); - border: 1px solid var(--vscode-input-border, var(--bs-border-color)); - border-radius: 2px; -} - -._labelChip_1nqcj_14 { - /* Label chip specific styles can go here */ -} - -._popoverContent_1nqcj_18 { - display: flex; - flex-direction: column; - gap: 0.5rem; - min-width: 200px; -} - -._popoverField_1nqcj_25 { - display: flex; - flex-direction: column; - gap: 0.2rem; -} - -._popoverLabel_1nqcj_31 { - font-weight: 600; -} - -._popoverActions_1nqcj_35 { - display: flex; - gap: 0.4rem; - justify-content: flex-end; - margin-top: 0.2rem; -} -._container_1cz5u_1 { - margin: 0.5rem; -} - -._traceback_1cz5u_5 { - margin-top: 1rem; -} -._table_z217i_1 { - width: 100%; -} - -._tableTokens_z217i_5 { - padding-bottom: 0.7rem; -} - -._tableH_z217i_9 { - padding: 0; - font-weight: 300; -} - -._model_z217i_14 { - padding-right: 1em; -} - -._cellContents_z217i_18 { - padding-bottom: 1em; -} -._scanInfo_76qrl_1 { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - margin-bottom: 1rem; -} - -._container_76qrl_7 { - padding: 0.5rem; -} -._container_1o5t7_1 { - padding: 1rem; - width: 100%; - height: 100%; -} -._list_5fj0m_1 { - width: 100%; - margin-top: 1em; -} - -._item_5fj0m_6 { - padding-bottom: 1em; -} -._header_m7jeh_1 { - display: flex; - align-items: center; - justify-content: space-between; - background-color: var(--bs-light-bg-subtle); - border-bottom: solid 1px var(--bs-border-color); - width: 100%; - padding-left: 0.5rem; -} - -._actions_m7jeh_11 { - display: flex; - align-items: center; - gap: 4px; - margin-right: 0.25rem; -} - -._iconButton_m7jeh_18 { - display: flex; - align-items: center; - justify-content: center; - width: 18px; - height: 18px; - font-size: 12px; - margin: 2px; - padding: 0; - background: transparent; - border: none; - border-radius: 4px; - color: var(--vscode-descriptionForeground); - cursor: pointer; - transition: - background-color 0.1s, - color 0.1s; -} - -._iconButton_m7jeh_18:hover { - background: var(--vscode-toolbar-hoverBackground); - color: var(--vscode-foreground); -} -._container_wqwns_1 { - height: 100%; - overflow-y: hidden; - display: flex; - flex-direction: column; -} - -._scrollable_wqwns_8 { - padding: 0.5rem; - overflow-y: auto; - flex: 1; - min-height: 0; -} -._container_19by0_1 { - display: grid; - grid-template-columns: 1fr 2fr; - overflow-y: hidden; - height: 100%; - width: 100%; - min-height: 0; -} - -._container_19by0_1 > * { - min-height: 0; -} -._sidebar_17smp_1 { - border-right: solid 1px var(--bs-border-color); - padding: 1rem; - overflow-y: auto; -} - -._container_17smp_7 { - display: grid; - grid-template-columns: max-content minmax(0, 1fr); - align-self: start; - gap: 0.5rem; -} - -._colspan_17smp_14 { - grid-column: span 2; -} - -._explanation_17smp_18 { - display: grid; - grid-template-columns: max-content 1fr; - column-gap: 1rem; - row-gap: 0.75rem; -} - -._scanValue_17smp_25 { - padding-right: 0.5rem; -} - -._values_17smp_29 { - display: flex; -} - -._validation_17smp_33 { - margin-left: 1rem; - display: grid; - grid-template-columns: max-content max-content; -} - -._validationLabel_17smp_39 { - padding-right: 0.5rem; - margin-top: 2px; -} -._header_1m0jc_1 { - padding-left: 1rem; - padding-right: 1rem; - display: grid; - padding-bottom: 1rem; - padding-top: 1rem; - border-top: solid var(--bs-light-border-subtle) 1px; - column-gap: 1rem; - grid-template-rows: max-content max-content; -} - -._value_1m0jc_12 { - word-wrap: break-all; - overflow-wrap: break-word; -} - -._oneCol_1m0jc_17 { - grid-template-columns: minmax(0, auto); -} - -._twoCol_1m0jc_21 { - grid-template-columns: minmax(0, auto) minmax(0, auto); -} - -._threeCol_1m0jc_25 { - grid-template-columns: minmax(0, auto) minmax(0, auto) minmax(0, auto); -} - -._fourCol_1m0jc_29 { - grid-template-columns: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax( - 0, - auto - ); -} - -._fiveCol_1m0jc_36 { - grid-template-columns: - minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) - minmax(0, auto); -} - -._sixCol_1m0jc_42 { - grid-template-columns: - minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) - minmax(0, auto) minmax(0, auto); -} -._container_1i4o7_1 { - display: grid; - grid-template-columns: max-content auto max-content; - align-items: center; - gap: 0.5rem; - margin-right: 1em; -} - -._nav_1i4o7_9:hover:not(._disabled_1i4o7_9) { - cursor: pointer; -} - -._disabled_1i4o7_9 { - opacity: 0.5; - pointer-events: none; -} - -._center_1i4o7_18 { - text-align: center; -} -._root_g13hf_1 { - height: 100%; - display: grid; - grid-template-rows: max-content max-content max-content 1fr; -} - -._scroller_g13hf_7 { - height: 100%; - width: 100%; - overflow-y: auto; -} - -._tabSet_g13hf_13 { - border-top: solid 1px var(--bs-border-color); - flex: 1; - min-height: 0; - overflow-y: hidden; - display: flex; - flex-direction: column; -} - -._tabControl_g13hf_22 { - margin: 0.2rem; -} - -._tabs_g13hf_26 { - margin-right: 1rem; -} - -._fullHeight_g13hf_30 { - height: 100%; -} - -._contentArea_g13hf_34 { - display: flex; - flex-direction: row; - flex: 1; - min-height: 0; - overflow: hidden; -} - -._tabSetWrapper_g13hf_42 { - flex: 1; - min-width: 0; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -._withValidation_g13hf_51 { - border-top: solid 1px var(--bs-border-color); -} - -._splitLayout_g13hf_55 { - height: 100%; - width: 100%; -} - -._splitStart_g13hf_60 { - height: 100%; - min-width: 0; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -._validationSidebar_g13hf_69 { - display: flex; - flex-direction: column; - height: 100%; - border-left: 1px solid var(--bs-border-color); - background-color: var(--bs-body-bg); - overflow-y: auto; -} -._container_1yw87_1 { - overflow-y: auto; - width: 100%; - height: 100%; -} -._eventRow_1j0jk_1 { - display: grid; - grid-template-columns: 10px 1fr; - column-gap: 3px; - cursor: pointer; -} - -._eventRow_1j0jk_1._selected_1j0jk_8 { - font-weight: 800; -} - -._eventRow_1j0jk_1 ._toggle_1j0jk_12 { - font-size: 0.7em; - margin-top: 4px; -} - -._eventLink_1j0jk_17 { - color: var(--bs-body-color); - text-decoration: none; - cursor: pointer; -} - -._eventLink_1j0jk_17:hover { - text-decoration: underline; - color: var(--bs-link-hover-color); -} - -._label_1j0jk_28 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -._icon_1j0jk_34 { - margin-right: 3px; -} - -._progress_1j0jk_38 { - margin-left: 0.3em !important; -} - -._popover_1j0jk_42 { - min-width: 300px; - max-width: 80%; -} -._node_1d5c2_1 { - display: grid; - column-gap: 0.3em; - grid-template-columns: max-content 1fr; -} - -._panel_1d5c2_7 { - margin-top: 0.65rem; - overflow: visible; -} -._tabContainer_8gcnx_1 { - display: grid; - grid-template-rows: max-content minmax(0, 1fr); - height: 100%; -} - -.nav._tabs_8gcnx_7 { - border-bottom: solid 1px var(--bs-border-color); - padding-bottom: 0.25rem; - padding-top: 0.5rem; - padding-left: 0.5rem; - padding-right: 0.5rem; - position: sticky; - z-index: 999; - top: 0; - background-color: var(--bs-body-bg); -} - -.nav._tabs_8gcnx_7 .nav-link { - padding: 0.3rem 0.5rem; - font-size: var(--inspect-font-size-smallest) !important; -} - -._chatTab_8gcnx_24 > * > * { - padding: 0.5rem; -} - -._chatTab_8gcnx_24 { - padding-bottom: 50px; -} - -._metadata_8gcnx_32 { - padding: 0.5rem; - padding-bottom: 50px; -} - -._scrollable_8gcnx_37 { - overflow-y: auto; - min-height: 0; - height: 100%; -} - -._eventsSeparator_8gcnx_43 { - background-color: var(--bs-border-color); -} - -._eventsList_8gcnx_47 { - padding-bottom: 1rem; -} - -._eventsContainer_8gcnx_51 { - display: grid; - width: 100%; - grid-template-columns: 180px 1px 1fr; - min-height: 100vh; - transition: grid-template-columns 0.3s ease; - padding-bottom: 44px; -} - -._eventsContainer_8gcnx_51._outlineCollapsed_8gcnx_60 { - grid-template-columns: 28px 1px 1fr; -} - -._eventsTab_8gcnx_64 { - display: flex; -} - -._eventsOutline_8gcnx_68 { - padding-left: 0.5rem; - padding-top: 0.8rem; -} - -._outlineToggle_8gcnx_73 { - cursor: pointer; - font-size: 0.8em; - position: absolute; - top: 0.5em; - right: 0.5em; -} - -._tabTool_8gcnx_81 { - font-size: var(--inspect-font-size-smallestest) !important; - padding: 0.1rem 0.4rem !important; -} - -._splitLayout_8gcnx_86 { - height: 100%; - width: 100%; -} - -._splitStart_8gcnx_91 { - height: 100%; - overflow-y: auto; -} - -._validationSidebar_8gcnx_96 { - display: flex; - flex-direction: column; - height: 100%; - border-left: 1px solid var(--bs-border-color); - background-color: var(--bs-body-bg); - overflow-y: auto; -} -._grid_1ml4j_1 { - display: grid; - grid-template-columns: 1fr 1fr; - column-gap: 2em; - row-gap: 0.15em; -} - -._row_1ml4j_8 { - display: flex; - align-items: center; - gap: 0.5em; - cursor: pointer; - border-radius: var(--bs-border-radius); - padding: 0.1em 0.4em; - margin: 0 -0.4em; -} - -._row_1ml4j_8:hover { - background-color: var(--bs-secondary-bg); -} - -._links_1ml4j_22 { - display: flex; - padding-bottom: 0.2em; - margin-bottom: 0.4em; - column-gap: 0.3em; - border-bottom: solid 1px var(--bs-border-color); -} - -._links_1ml4j_22 a { - cursor: pointer; - text-decoration: none; - color: var(--bs-link-color); -} - -._links_1ml4j_22 a:hover { - color: var(--bs-link-hover-color); -} - -._selected_1ml4j_40 { - font-weight: 600; -} -._container_godvv_1 { - display: grid; - grid-template-rows: max-content max-content 1fr; - height: 100vh; -} - -._transcriptContainer_godvv_7 { - overflow-y: auto; - width: 100%; - min-height: 0; -} -._headingGrid_1n1sm_1 { - display: grid; - gap: 1rem; -} - -._headingCell_1n1sm_6 { - display: flex; - align-items: flex-start; - min-width: 0; /* Allow flex items to shrink below their content size */ -} - -/* Horizontal layouts (left/right) */ -._headingCell_1n1sm_6._horizontal_1n1sm_13 { - flex-direction: row; - gap: 0.5rem; -} - -/* Vertical layouts (above/below) */ -._headingCell_1n1sm_6._vertical_1n1sm_19 { - flex-direction: column; - align-items: flex-start; - gap: 0rem; -} - -._label_1n1sm_25 { - font-weight: 500; - white-space: nowrap; - flex-shrink: 0; /* Prevent labels from shrinking */ -} - -._value_1n1sm_31 { - min-width: 0; /* Allow values to shrink */ - word-break: break-word; /* Allow long words to break */ -} - -/* For vertical layouts, allow value to take full width */ -._vertical_1n1sm_19 ._value_1n1sm_31 { - width: 100%; -} -._titleContainer_6mwxv_1 { - margin-left: 0.5rem; - margin-right: 0.5rem; - margin-top: 0.5rem; - margin-bottom: 1rem; -} -.findBand { - position: absolute; - top: 0; - right: 0; - margin-right: 20%; - z-index: 1060; - color: var(--inspect-find-foreground); - background-color: var(--inspect-find-background); - font-size: 0.9rem; - display: grid; - grid-template-columns: auto auto auto auto auto; - column-gap: 0.2em; - padding: 0.2rem; - border-bottom: solid 1px var(--bs-light-border-subtle); - border-left: solid 1px var(--bs-light-border-subtle); - border-right: solid 1px var(--bs-light-border-subtle); - box-shadow: var(--bs-box-shadow); -} - -.findBand input { - height: 2em; - font-size: 0.9em; - margin: 0.1rem; - outline: none; - border: solid 1px var(--inspect-input-border); - color: var(--inspect-input-foreground); - background: var(--inspect-input-background); -} - -#inspect-find-no-results { - font-size: 0.9em; - opacity: 0; - margin-top: auto; - margin-bottom: auto; - margin-right: 0.5em; -} - -.findBand .btn.next, -.findBand .btn.prev { - padding: 0; - font-size: var(--inspect-fond-size-larger); -} - -.findBand .btn.close { - padding: 0; - font-size: var(--inspect-font-size-title-secondary); - margin-top: -0.1rem; - margin-bottom: -0.1rem; -} diff --git a/src/inspect_scout/_view/www/dist/assets/index.js b/src/inspect_scout/_view/www/dist/assets/index.js deleted file mode 100644 index adf6c8772..000000000 --- a/src/inspect_scout/_view/www/dist/assets/index.js +++ /dev/null @@ -1,171879 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./lib-CBtriEt5.js","./chunk-DfAF0w94.js","./wgxpath.install-node-Csk64Aj9.js","./liteDOM-Cp0aN3bP.js","./_commonjsHelpers.js","./xypic-DrMJn58R.js","./tex-svg-full-BI3fonbT.js"])))=>i.map(i=>d[i]); -import { g as getDefaultExportFromCjs, c as commonjsGlobal } from "./_commonjsHelpers.js"; -import { l as l$2 } from "./chunk-DfAF0w94.js"; -function _mergeNamespaces(n3, m2) { - for (var i4 = 0; i4 < m2.length; i4++) { - const e3 = m2[i4]; - if (typeof e3 !== "string" && !Array.isArray(e3)) { - for (const k3 in e3) { - if (k3 !== "default" && !(k3 in n3)) { - const d2 = Object.getOwnPropertyDescriptor(e3, k3); - if (d2) { - Object.defineProperty(n3, k3, d2.get ? d2 : { - enumerable: true, - get: () => e3[k3] - }); - } - } - } - } - } - return Object.freeze(Object.defineProperty(n3, Symbol.toStringTag, { value: "Module" })); -} -(function polyfill() { - const relList = document.createElement("link").relList; - if (relList && relList.supports && relList.supports("modulepreload")) return; - for (const link2 of document.querySelectorAll('link[rel="modulepreload"]')) processPreload(link2); - new MutationObserver((mutations) => { - for (const mutation of mutations) { - if (mutation.type !== "childList") continue; - for (const node2 of mutation.addedNodes) if (node2.tagName === "LINK" && node2.rel === "modulepreload") processPreload(node2); - } - }).observe(document, { - childList: true, - subtree: true - }); - function getFetchOpts(link2) { - const fetchOpts = {}; - if (link2.integrity) fetchOpts.integrity = link2.integrity; - if (link2.referrerPolicy) fetchOpts.referrerPolicy = link2.referrerPolicy; - if (link2.crossOrigin === "use-credentials") fetchOpts.credentials = "include"; - else if (link2.crossOrigin === "anonymous") fetchOpts.credentials = "omit"; - else fetchOpts.credentials = "same-origin"; - return fetchOpts; - } - function processPreload(link2) { - if (link2.ep) return; - link2.ep = true; - const fetchOpts = getFetchOpts(link2); - fetch(link2.href, fetchOpts); - } -})(); -var jsxRuntime = { exports: {} }; -var reactJsxRuntime_production = {}; -var hasRequiredReactJsxRuntime_production; -function requireReactJsxRuntime_production() { - if (hasRequiredReactJsxRuntime_production) return reactJsxRuntime_production; - hasRequiredReactJsxRuntime_production = 1; - var REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.transitional.element"), REACT_FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for("react.fragment"); - function jsxProd(type, config2, maybeKey) { - var key2 = null; - void 0 !== maybeKey && (key2 = "" + maybeKey); - void 0 !== config2.key && (key2 = "" + config2.key); - if ("key" in config2) { - maybeKey = {}; - for (var propName in config2) - "key" !== propName && (maybeKey[propName] = config2[propName]); - } else maybeKey = config2; - config2 = maybeKey.ref; - return { - $$typeof: REACT_ELEMENT_TYPE, - type, - key: key2, - ref: void 0 !== config2 ? config2 : null, - props: maybeKey - }; - } - reactJsxRuntime_production.Fragment = REACT_FRAGMENT_TYPE; - reactJsxRuntime_production.jsx = jsxProd; - reactJsxRuntime_production.jsxs = jsxProd; - return reactJsxRuntime_production; -} -var hasRequiredJsxRuntime; -function requireJsxRuntime() { - if (hasRequiredJsxRuntime) return jsxRuntime.exports; - hasRequiredJsxRuntime = 1; - { - jsxRuntime.exports = requireReactJsxRuntime_production(); - } - return jsxRuntime.exports; -} -var jsxRuntimeExports = requireJsxRuntime(); -var Subscribable = class { - constructor() { - this.listeners = /* @__PURE__ */ new Set(); - this.subscribe = this.subscribe.bind(this); - } - subscribe(listener) { - this.listeners.add(listener); - this.onSubscribe(); - return () => { - this.listeners.delete(listener); - this.onUnsubscribe(); - }; - } - hasListeners() { - return this.listeners.size > 0; - } - onSubscribe() { - } - onUnsubscribe() { - } -}; -var defaultTimeoutProvider = { - // We need the wrapper function syntax below instead of direct references to - // global setTimeout etc. - // - // BAD: `setTimeout: setTimeout` - // GOOD: `setTimeout: (cb, delay) => setTimeout(cb, delay)` - // - // If we use direct references here, then anything that wants to spy on or - // replace the global setTimeout (like tests) won't work since we'll already - // have a hard reference to the original implementation at the time when this - // file was imported. - setTimeout: (callback, delay) => setTimeout(callback, delay), - clearTimeout: (timeoutId) => clearTimeout(timeoutId), - setInterval: (callback, delay) => setInterval(callback, delay), - clearInterval: (intervalId) => clearInterval(intervalId) -}; -var TimeoutManager = class { - // We cannot have TimeoutManager as we must instantiate it with a concrete - // type at app boot; and if we leave that type, then any new timer provider - // would need to support ReturnType, which is infeasible. - // - // We settle for type safety for the TimeoutProvider type, and accept that - // this class is unsafe internally to allow for extension. - #provider = defaultTimeoutProvider; - #providerCalled = false; - setTimeoutProvider(provider) { - this.#provider = provider; - } - setTimeout(callback, delay) { - return this.#provider.setTimeout(callback, delay); - } - clearTimeout(timeoutId) { - this.#provider.clearTimeout(timeoutId); - } - setInterval(callback, delay) { - return this.#provider.setInterval(callback, delay); - } - clearInterval(intervalId) { - this.#provider.clearInterval(intervalId); - } -}; -var timeoutManager = new TimeoutManager(); -function systemSetTimeoutZero(callback) { - setTimeout(callback, 0); -} -var isServer = typeof window === "undefined" || "Deno" in globalThis; -function noop$2() { -} -function functionalUpdate$1(updater, input2) { - return typeof updater === "function" ? updater(input2) : updater; -} -function isValidTimeout(value2) { - return typeof value2 === "number" && value2 >= 0 && value2 !== Infinity; -} -function timeUntilStale(updatedAt, staleTime) { - return Math.max(updatedAt + (staleTime || 0) - Date.now(), 0); -} -function resolveStaleTime(staleTime, query2) { - return typeof staleTime === "function" ? staleTime(query2) : staleTime; -} -function resolveEnabled(enabled, query2) { - return typeof enabled === "function" ? enabled(query2) : enabled; -} -function matchQuery(filters, query2) { - const { - type = "all", - exact, - fetchStatus, - predicate, - queryKey, - stale - } = filters; - if (queryKey) { - if (exact) { - if (query2.queryHash !== hashQueryKeyByOptions(queryKey, query2.options)) { - return false; - } - } else if (!partialMatchKey(query2.queryKey, queryKey)) { - return false; - } - } - if (type !== "all") { - const isActive = query2.isActive(); - if (type === "active" && !isActive) { - return false; - } - if (type === "inactive" && isActive) { - return false; - } - } - if (typeof stale === "boolean" && query2.isStale() !== stale) { - return false; - } - if (fetchStatus && fetchStatus !== query2.state.fetchStatus) { - return false; - } - if (predicate && !predicate(query2)) { - return false; - } - return true; -} -function matchMutation(filters, mutation) { - const { exact, status, predicate, mutationKey } = filters; - if (mutationKey) { - if (!mutation.options.mutationKey) { - return false; - } - if (exact) { - if (hashKey(mutation.options.mutationKey) !== hashKey(mutationKey)) { - return false; - } - } else if (!partialMatchKey(mutation.options.mutationKey, mutationKey)) { - return false; - } - } - if (status && mutation.state.status !== status) { - return false; - } - if (predicate && !predicate(mutation)) { - return false; - } - return true; -} -function hashQueryKeyByOptions(queryKey, options) { - const hashFn = options?.queryKeyHashFn || hashKey; - return hashFn(queryKey); -} -function hashKey(queryKey) { - return JSON.stringify( - queryKey, - (_2, val) => isPlainObject$2(val) ? Object.keys(val).sort().reduce((result2, key2) => { - result2[key2] = val[key2]; - return result2; - }, {}) : val - ); -} -function partialMatchKey(a2, b2) { - if (a2 === b2) { - return true; - } - if (typeof a2 !== typeof b2) { - return false; - } - if (a2 && b2 && typeof a2 === "object" && typeof b2 === "object") { - return Object.keys(b2).every((key2) => partialMatchKey(a2[key2], b2[key2])); - } - return false; -} -var hasOwn$1 = Object.prototype.hasOwnProperty; -function replaceEqualDeep(a2, b2) { - if (a2 === b2) { - return a2; - } - const array2 = isPlainArray(a2) && isPlainArray(b2); - if (!array2 && !(isPlainObject$2(a2) && isPlainObject$2(b2))) return b2; - const aItems = array2 ? a2 : Object.keys(a2); - const aSize = aItems.length; - const bItems = array2 ? b2 : Object.keys(b2); - const bSize = bItems.length; - const copy = array2 ? new Array(bSize) : {}; - let equalItems = 0; - for (let i4 = 0; i4 < bSize; i4++) { - const key2 = array2 ? i4 : bItems[i4]; - const aItem = a2[key2]; - const bItem = b2[key2]; - if (aItem === bItem) { - copy[key2] = aItem; - if (array2 ? i4 < aSize : hasOwn$1.call(a2, key2)) equalItems++; - continue; - } - if (aItem === null || bItem === null || typeof aItem !== "object" || typeof bItem !== "object") { - copy[key2] = bItem; - continue; - } - const v2 = replaceEqualDeep(aItem, bItem); - copy[key2] = v2; - if (v2 === aItem) equalItems++; - } - return aSize === bSize && equalItems === aSize ? a2 : copy; -} -function shallowEqualObjects(a2, b2) { - if (!b2 || Object.keys(a2).length !== Object.keys(b2).length) { - return false; - } - for (const key2 in a2) { - if (a2[key2] !== b2[key2]) { - return false; - } - } - return true; -} -function isPlainArray(value2) { - return Array.isArray(value2) && value2.length === Object.keys(value2).length; -} -function isPlainObject$2(o2) { - if (!hasObjectPrototype(o2)) { - return false; - } - const ctor = o2.constructor; - if (ctor === void 0) { - return true; - } - const prot = ctor.prototype; - if (!hasObjectPrototype(prot)) { - return false; - } - if (!prot.hasOwnProperty("isPrototypeOf")) { - return false; - } - if (Object.getPrototypeOf(o2) !== Object.prototype) { - return false; - } - return true; -} -function hasObjectPrototype(o2) { - return Object.prototype.toString.call(o2) === "[object Object]"; -} -function sleep$1(timeout) { - return new Promise((resolve2) => { - timeoutManager.setTimeout(resolve2, timeout); - }); -} -function replaceData(prevData, data2, options) { - if (typeof options.structuralSharing === "function") { - return options.structuralSharing(prevData, data2); - } else if (options.structuralSharing !== false) { - return replaceEqualDeep(prevData, data2); - } - return data2; -} -function keepPreviousData(previousData) { - return previousData; -} -function addToEnd(items, item2, max2 = 0) { - const newItems = [...items, item2]; - return max2 && newItems.length > max2 ? newItems.slice(1) : newItems; -} -function addToStart(items, item2, max2 = 0) { - const newItems = [item2, ...items]; - return max2 && newItems.length > max2 ? newItems.slice(0, -1) : newItems; -} -var skipToken = /* @__PURE__ */ Symbol(); -function ensureQueryFn(options, fetchOptions) { - if (!options.queryFn && fetchOptions?.initialPromise) { - return () => fetchOptions.initialPromise; - } - if (!options.queryFn || options.queryFn === skipToken) { - return () => Promise.reject(new Error(`Missing queryFn: '${options.queryHash}'`)); - } - return options.queryFn; -} -function shouldThrowError(throwOnError, params) { - if (typeof throwOnError === "function") { - return throwOnError(...params); - } - return !!throwOnError; -} -var FocusManager = class extends Subscribable { - #focused; - #cleanup; - #setup; - constructor() { - super(); - this.#setup = (onFocus) => { - if (!isServer && window.addEventListener) { - const listener = () => onFocus(); - window.addEventListener("visibilitychange", listener, false); - return () => { - window.removeEventListener("visibilitychange", listener); - }; - } - return; - }; - } - onSubscribe() { - if (!this.#cleanup) { - this.setEventListener(this.#setup); - } - } - onUnsubscribe() { - if (!this.hasListeners()) { - this.#cleanup?.(); - this.#cleanup = void 0; - } - } - setEventListener(setup) { - this.#setup = setup; - this.#cleanup?.(); - this.#cleanup = setup((focused) => { - if (typeof focused === "boolean") { - this.setFocused(focused); - } else { - this.onFocus(); - } - }); - } - setFocused(focused) { - const changed = this.#focused !== focused; - if (changed) { - this.#focused = focused; - this.onFocus(); - } - } - onFocus() { - const isFocused = this.isFocused(); - this.listeners.forEach((listener) => { - listener(isFocused); - }); - } - isFocused() { - if (typeof this.#focused === "boolean") { - return this.#focused; - } - return globalThis.document?.visibilityState !== "hidden"; - } -}; -var focusManager = new FocusManager(); -function pendingThenable() { - let resolve2; - let reject; - const thenable = new Promise((_resolve, _reject) => { - resolve2 = _resolve; - reject = _reject; - }); - thenable.status = "pending"; - thenable.catch(() => { - }); - function finalize2(data2) { - Object.assign(thenable, data2); - delete thenable.resolve; - delete thenable.reject; - } - thenable.resolve = (value2) => { - finalize2({ - status: "fulfilled", - value: value2 - }); - resolve2(value2); - }; - thenable.reject = (reason) => { - finalize2({ - status: "rejected", - reason - }); - reject(reason); - }; - return thenable; -} -var defaultScheduler = systemSetTimeoutZero; -function createNotifyManager() { - let queue = []; - let transactions = 0; - let notifyFn = (callback) => { - callback(); - }; - let batchNotifyFn = (callback) => { - callback(); - }; - let scheduleFn = defaultScheduler; - const schedule = (callback) => { - if (transactions) { - queue.push(callback); - } else { - scheduleFn(() => { - notifyFn(callback); - }); - } - }; - const flush = () => { - const originalQueue = queue; - queue = []; - if (originalQueue.length) { - scheduleFn(() => { - batchNotifyFn(() => { - originalQueue.forEach((callback) => { - notifyFn(callback); - }); - }); - }); - } - }; - return { - batch: (callback) => { - let result2; - transactions++; - try { - result2 = callback(); - } finally { - transactions--; - if (!transactions) { - flush(); - } - } - return result2; - }, - /** - * All calls to the wrapped function will be batched. - */ - batchCalls: (callback) => { - return (...args2) => { - schedule(() => { - callback(...args2); - }); - }; - }, - schedule, - /** - * Use this method to set a custom notify function. - * This can be used to for example wrap notifications with `React.act` while running tests. - */ - setNotifyFunction: (fn3) => { - notifyFn = fn3; - }, - /** - * Use this method to set a custom function to batch notifications together into a single tick. - * By default React Query will use the batch function provided by ReactDOM or React Native. - */ - setBatchNotifyFunction: (fn3) => { - batchNotifyFn = fn3; - }, - setScheduler: (fn3) => { - scheduleFn = fn3; - } - }; -} -var notifyManager = createNotifyManager(); -var OnlineManager = class extends Subscribable { - #online = true; - #cleanup; - #setup; - constructor() { - super(); - this.#setup = (onOnline) => { - if (!isServer && window.addEventListener) { - const onlineListener = () => onOnline(true); - const offlineListener = () => onOnline(false); - window.addEventListener("online", onlineListener, false); - window.addEventListener("offline", offlineListener, false); - return () => { - window.removeEventListener("online", onlineListener); - window.removeEventListener("offline", offlineListener); - }; - } - return; - }; - } - onSubscribe() { - if (!this.#cleanup) { - this.setEventListener(this.#setup); - } - } - onUnsubscribe() { - if (!this.hasListeners()) { - this.#cleanup?.(); - this.#cleanup = void 0; - } - } - setEventListener(setup) { - this.#setup = setup; - this.#cleanup?.(); - this.#cleanup = setup(this.setOnline.bind(this)); - } - setOnline(online) { - const changed = this.#online !== online; - if (changed) { - this.#online = online; - this.listeners.forEach((listener) => { - listener(online); - }); - } - } - isOnline() { - return this.#online; - } -}; -var onlineManager = new OnlineManager(); -function defaultRetryDelay(failureCount) { - return Math.min(1e3 * 2 ** failureCount, 3e4); -} -function canFetch(networkMode) { - return (networkMode ?? "online") === "online" ? onlineManager.isOnline() : true; -} -var CancelledError = class extends Error { - constructor(options) { - super("CancelledError"); - this.revert = options?.revert; - this.silent = options?.silent; - } -}; -function createRetryer(config2) { - let isRetryCancelled = false; - let failureCount = 0; - let continueFn; - const thenable = pendingThenable(); - const isResolved = () => thenable.status !== "pending"; - const cancel = (cancelOptions) => { - if (!isResolved()) { - const error2 = new CancelledError(cancelOptions); - reject(error2); - config2.onCancel?.(error2); - } - }; - const cancelRetry = () => { - isRetryCancelled = true; - }; - const continueRetry = () => { - isRetryCancelled = false; - }; - const canContinue = () => focusManager.isFocused() && (config2.networkMode === "always" || onlineManager.isOnline()) && config2.canRun(); - const canStart = () => canFetch(config2.networkMode) && config2.canRun(); - const resolve2 = (value2) => { - if (!isResolved()) { - continueFn?.(); - thenable.resolve(value2); - } - }; - const reject = (value2) => { - if (!isResolved()) { - continueFn?.(); - thenable.reject(value2); - } - }; - const pause = () => { - return new Promise((continueResolve) => { - continueFn = (value2) => { - if (isResolved() || canContinue()) { - continueResolve(value2); - } - }; - config2.onPause?.(); - }).then(() => { - continueFn = void 0; - if (!isResolved()) { - config2.onContinue?.(); - } - }); - }; - const run = () => { - if (isResolved()) { - return; - } - let promiseOrValue; - const initialPromise = failureCount === 0 ? config2.initialPromise : void 0; - try { - promiseOrValue = initialPromise ?? config2.fn(); - } catch (error2) { - promiseOrValue = Promise.reject(error2); - } - Promise.resolve(promiseOrValue).then(resolve2).catch((error2) => { - if (isResolved()) { - return; - } - const retry = config2.retry ?? (isServer ? 0 : 3); - const retryDelay = config2.retryDelay ?? defaultRetryDelay; - const delay = typeof retryDelay === "function" ? retryDelay(failureCount, error2) : retryDelay; - const shouldRetry = retry === true || typeof retry === "number" && failureCount < retry || typeof retry === "function" && retry(failureCount, error2); - if (isRetryCancelled || !shouldRetry) { - reject(error2); - return; - } - failureCount++; - config2.onFail?.(failureCount, error2); - sleep$1(delay).then(() => { - return canContinue() ? void 0 : pause(); - }).then(() => { - if (isRetryCancelled) { - reject(error2); - } else { - run(); - } - }); - }); - }; - return { - promise: thenable, - status: () => thenable.status, - cancel, - continue: () => { - continueFn?.(); - return thenable; - }, - cancelRetry, - continueRetry, - canStart, - start: () => { - if (canStart()) { - run(); - } else { - pause().then(run); - } - return thenable; - } - }; -} -var Removable = class { - #gcTimeout; - destroy() { - this.clearGcTimeout(); - } - scheduleGc() { - this.clearGcTimeout(); - if (isValidTimeout(this.gcTime)) { - this.#gcTimeout = timeoutManager.setTimeout(() => { - this.optionalRemove(); - }, this.gcTime); - } - } - updateGcTime(newGcTime) { - this.gcTime = Math.max( - this.gcTime || 0, - newGcTime ?? (isServer ? Infinity : 5 * 60 * 1e3) - ); - } - clearGcTimeout() { - if (this.#gcTimeout) { - timeoutManager.clearTimeout(this.#gcTimeout); - this.#gcTimeout = void 0; - } - } -}; -var Query = class extends Removable { - #initialState; - #revertState; - #cache; - #client; - #retryer; - #defaultOptions; - #abortSignalConsumed; - constructor(config2) { - super(); - this.#abortSignalConsumed = false; - this.#defaultOptions = config2.defaultOptions; - this.setOptions(config2.options); - this.observers = []; - this.#client = config2.client; - this.#cache = this.#client.getQueryCache(); - this.queryKey = config2.queryKey; - this.queryHash = config2.queryHash; - this.#initialState = getDefaultState$1(this.options); - this.state = config2.state ?? this.#initialState; - this.scheduleGc(); - } - get meta() { - return this.options.meta; - } - get promise() { - return this.#retryer?.promise; - } - setOptions(options) { - this.options = { ...this.#defaultOptions, ...options }; - this.updateGcTime(this.options.gcTime); - if (this.state && this.state.data === void 0) { - const defaultState = getDefaultState$1(this.options); - if (defaultState.data !== void 0) { - this.setState( - successState(defaultState.data, defaultState.dataUpdatedAt) - ); - this.#initialState = defaultState; - } - } - } - optionalRemove() { - if (!this.observers.length && this.state.fetchStatus === "idle") { - this.#cache.remove(this); - } - } - setData(newData, options) { - const data2 = replaceData(this.state.data, newData, this.options); - this.#dispatch({ - data: data2, - type: "success", - dataUpdatedAt: options?.updatedAt, - manual: options?.manual - }); - return data2; - } - setState(state, setStateOptions) { - this.#dispatch({ type: "setState", state, setStateOptions }); - } - cancel(options) { - const promise = this.#retryer?.promise; - this.#retryer?.cancel(options); - return promise ? promise.then(noop$2).catch(noop$2) : Promise.resolve(); - } - destroy() { - super.destroy(); - this.cancel({ silent: true }); - } - reset() { - this.destroy(); - this.setState(this.#initialState); - } - isActive() { - return this.observers.some( - (observer) => resolveEnabled(observer.options.enabled, this) !== false - ); - } - isDisabled() { - if (this.getObserversCount() > 0) { - return !this.isActive(); - } - return this.options.queryFn === skipToken || this.state.dataUpdateCount + this.state.errorUpdateCount === 0; - } - isStatic() { - if (this.getObserversCount() > 0) { - return this.observers.some( - (observer) => resolveStaleTime(observer.options.staleTime, this) === "static" - ); - } - return false; - } - isStale() { - if (this.getObserversCount() > 0) { - return this.observers.some( - (observer) => observer.getCurrentResult().isStale - ); - } - return this.state.data === void 0 || this.state.isInvalidated; - } - isStaleByTime(staleTime = 0) { - if (this.state.data === void 0) { - return true; - } - if (staleTime === "static") { - return false; - } - if (this.state.isInvalidated) { - return true; - } - return !timeUntilStale(this.state.dataUpdatedAt, staleTime); - } - onFocus() { - const observer = this.observers.find((x2) => x2.shouldFetchOnWindowFocus()); - observer?.refetch({ cancelRefetch: false }); - this.#retryer?.continue(); - } - onOnline() { - const observer = this.observers.find((x2) => x2.shouldFetchOnReconnect()); - observer?.refetch({ cancelRefetch: false }); - this.#retryer?.continue(); - } - addObserver(observer) { - if (!this.observers.includes(observer)) { - this.observers.push(observer); - this.clearGcTimeout(); - this.#cache.notify({ type: "observerAdded", query: this, observer }); - } - } - removeObserver(observer) { - if (this.observers.includes(observer)) { - this.observers = this.observers.filter((x2) => x2 !== observer); - if (!this.observers.length) { - if (this.#retryer) { - if (this.#abortSignalConsumed) { - this.#retryer.cancel({ revert: true }); - } else { - this.#retryer.cancelRetry(); - } - } - this.scheduleGc(); - } - this.#cache.notify({ type: "observerRemoved", query: this, observer }); - } - } - getObserversCount() { - return this.observers.length; - } - invalidate() { - if (!this.state.isInvalidated) { - this.#dispatch({ type: "invalidate" }); - } - } - async fetch(options, fetchOptions) { - if (this.state.fetchStatus !== "idle" && // If the promise in the retyer is already rejected, we have to definitely - // re-start the fetch; there is a chance that the query is still in a - // pending state when that happens - this.#retryer?.status() !== "rejected") { - if (this.state.data !== void 0 && fetchOptions?.cancelRefetch) { - this.cancel({ silent: true }); - } else if (this.#retryer) { - this.#retryer.continueRetry(); - return this.#retryer.promise; - } - } - if (options) { - this.setOptions(options); - } - if (!this.options.queryFn) { - const observer = this.observers.find((x2) => x2.options.queryFn); - if (observer) { - this.setOptions(observer.options); - } - } - const abortController = new AbortController(); - const addSignalProperty = (object2) => { - Object.defineProperty(object2, "signal", { - enumerable: true, - get: () => { - this.#abortSignalConsumed = true; - return abortController.signal; - } - }); - }; - const fetchFn = () => { - const queryFn = ensureQueryFn(this.options, fetchOptions); - const createQueryFnContext = () => { - const queryFnContext2 = { - client: this.#client, - queryKey: this.queryKey, - meta: this.meta - }; - addSignalProperty(queryFnContext2); - return queryFnContext2; - }; - const queryFnContext = createQueryFnContext(); - this.#abortSignalConsumed = false; - if (this.options.persister) { - return this.options.persister( - queryFn, - queryFnContext, - this - ); - } - return queryFn(queryFnContext); - }; - const createFetchContext = () => { - const context2 = { - fetchOptions, - options: this.options, - queryKey: this.queryKey, - client: this.#client, - state: this.state, - fetchFn - }; - addSignalProperty(context2); - return context2; - }; - const context = createFetchContext(); - this.options.behavior?.onFetch(context, this); - this.#revertState = this.state; - if (this.state.fetchStatus === "idle" || this.state.fetchMeta !== context.fetchOptions?.meta) { - this.#dispatch({ type: "fetch", meta: context.fetchOptions?.meta }); - } - this.#retryer = createRetryer({ - initialPromise: fetchOptions?.initialPromise, - fn: context.fetchFn, - onCancel: (error2) => { - if (error2 instanceof CancelledError && error2.revert) { - this.setState({ - ...this.#revertState, - fetchStatus: "idle" - }); - } - abortController.abort(); - }, - onFail: (failureCount, error2) => { - this.#dispatch({ type: "failed", failureCount, error: error2 }); - }, - onPause: () => { - this.#dispatch({ type: "pause" }); - }, - onContinue: () => { - this.#dispatch({ type: "continue" }); - }, - retry: context.options.retry, - retryDelay: context.options.retryDelay, - networkMode: context.options.networkMode, - canRun: () => true - }); - try { - const data2 = await this.#retryer.start(); - if (data2 === void 0) { - if (false) ; - throw new Error(`${this.queryHash} data is undefined`); - } - this.setData(data2); - this.#cache.config.onSuccess?.(data2, this); - this.#cache.config.onSettled?.( - data2, - this.state.error, - this - ); - return data2; - } catch (error2) { - if (error2 instanceof CancelledError) { - if (error2.silent) { - return this.#retryer.promise; - } else if (error2.revert) { - if (this.state.data === void 0) { - throw error2; - } - return this.state.data; - } - } - this.#dispatch({ - type: "error", - error: error2 - }); - this.#cache.config.onError?.( - error2, - this - ); - this.#cache.config.onSettled?.( - this.state.data, - error2, - this - ); - throw error2; - } finally { - this.scheduleGc(); - } - } - #dispatch(action2) { - const reducer = (state) => { - switch (action2.type) { - case "failed": - return { - ...state, - fetchFailureCount: action2.failureCount, - fetchFailureReason: action2.error - }; - case "pause": - return { - ...state, - fetchStatus: "paused" - }; - case "continue": - return { - ...state, - fetchStatus: "fetching" - }; - case "fetch": - return { - ...state, - ...fetchState(state.data, this.options), - fetchMeta: action2.meta ?? null - }; - case "success": - const newState = { - ...state, - ...successState(action2.data, action2.dataUpdatedAt), - dataUpdateCount: state.dataUpdateCount + 1, - ...!action2.manual && { - fetchStatus: "idle", - fetchFailureCount: 0, - fetchFailureReason: null - } - }; - this.#revertState = action2.manual ? newState : void 0; - return newState; - case "error": - const error2 = action2.error; - return { - ...state, - error: error2, - errorUpdateCount: state.errorUpdateCount + 1, - errorUpdatedAt: Date.now(), - fetchFailureCount: state.fetchFailureCount + 1, - fetchFailureReason: error2, - fetchStatus: "idle", - status: "error" - }; - case "invalidate": - return { - ...state, - isInvalidated: true - }; - case "setState": - return { - ...state, - ...action2.state - }; - } - }; - this.state = reducer(this.state); - notifyManager.batch(() => { - this.observers.forEach((observer) => { - observer.onQueryUpdate(); - }); - this.#cache.notify({ query: this, type: "updated", action: action2 }); - }); - } -}; -function fetchState(data2, options) { - return { - fetchFailureCount: 0, - fetchFailureReason: null, - fetchStatus: canFetch(options.networkMode) ? "fetching" : "paused", - ...data2 === void 0 && { - error: null, - status: "pending" - } - }; -} -function successState(data2, dataUpdatedAt) { - return { - data: data2, - dataUpdatedAt: dataUpdatedAt ?? Date.now(), - error: null, - isInvalidated: false, - status: "success" - }; -} -function getDefaultState$1(options) { - const data2 = typeof options.initialData === "function" ? options.initialData() : options.initialData; - const hasData = data2 !== void 0; - const initialDataUpdatedAt = hasData ? typeof options.initialDataUpdatedAt === "function" ? options.initialDataUpdatedAt() : options.initialDataUpdatedAt : 0; - return { - data: data2, - dataUpdateCount: 0, - dataUpdatedAt: hasData ? initialDataUpdatedAt ?? Date.now() : 0, - error: null, - errorUpdateCount: 0, - errorUpdatedAt: 0, - fetchFailureCount: 0, - fetchFailureReason: null, - fetchMeta: null, - isInvalidated: false, - status: hasData ? "success" : "pending", - fetchStatus: "idle" - }; -} -var QueryObserver = class extends Subscribable { - constructor(client2, options) { - super(); - this.options = options; - this.#client = client2; - this.#selectError = null; - this.#currentThenable = pendingThenable(); - this.bindMethods(); - this.setOptions(options); - } - #client; - #currentQuery = void 0; - #currentQueryInitialState = void 0; - #currentResult = void 0; - #currentResultState; - #currentResultOptions; - #currentThenable; - #selectError; - #selectFn; - #selectResult; - // This property keeps track of the last query with defined data. - // It will be used to pass the previous data and query to the placeholder function between renders. - #lastQueryWithDefinedData; - #staleTimeoutId; - #refetchIntervalId; - #currentRefetchInterval; - #trackedProps = /* @__PURE__ */ new Set(); - bindMethods() { - this.refetch = this.refetch.bind(this); - } - onSubscribe() { - if (this.listeners.size === 1) { - this.#currentQuery.addObserver(this); - if (shouldFetchOnMount(this.#currentQuery, this.options)) { - this.#executeFetch(); - } else { - this.updateResult(); - } - this.#updateTimers(); - } - } - onUnsubscribe() { - if (!this.hasListeners()) { - this.destroy(); - } - } - shouldFetchOnReconnect() { - return shouldFetchOn( - this.#currentQuery, - this.options, - this.options.refetchOnReconnect - ); - } - shouldFetchOnWindowFocus() { - return shouldFetchOn( - this.#currentQuery, - this.options, - this.options.refetchOnWindowFocus - ); - } - destroy() { - this.listeners = /* @__PURE__ */ new Set(); - this.#clearStaleTimeout(); - this.#clearRefetchInterval(); - this.#currentQuery.removeObserver(this); - } - setOptions(options) { - const prevOptions = this.options; - const prevQuery = this.#currentQuery; - this.options = this.#client.defaultQueryOptions(options); - if (this.options.enabled !== void 0 && typeof this.options.enabled !== "boolean" && typeof this.options.enabled !== "function" && typeof resolveEnabled(this.options.enabled, this.#currentQuery) !== "boolean") { - throw new Error( - "Expected enabled to be a boolean or a callback that returns a boolean" - ); - } - this.#updateQuery(); - this.#currentQuery.setOptions(this.options); - if (prevOptions._defaulted && !shallowEqualObjects(this.options, prevOptions)) { - this.#client.getQueryCache().notify({ - type: "observerOptionsUpdated", - query: this.#currentQuery, - observer: this - }); - } - const mounted = this.hasListeners(); - if (mounted && shouldFetchOptionally( - this.#currentQuery, - prevQuery, - this.options, - prevOptions - )) { - this.#executeFetch(); - } - this.updateResult(); - if (mounted && (this.#currentQuery !== prevQuery || resolveEnabled(this.options.enabled, this.#currentQuery) !== resolveEnabled(prevOptions.enabled, this.#currentQuery) || resolveStaleTime(this.options.staleTime, this.#currentQuery) !== resolveStaleTime(prevOptions.staleTime, this.#currentQuery))) { - this.#updateStaleTimeout(); - } - const nextRefetchInterval = this.#computeRefetchInterval(); - if (mounted && (this.#currentQuery !== prevQuery || resolveEnabled(this.options.enabled, this.#currentQuery) !== resolveEnabled(prevOptions.enabled, this.#currentQuery) || nextRefetchInterval !== this.#currentRefetchInterval)) { - this.#updateRefetchInterval(nextRefetchInterval); - } - } - getOptimisticResult(options) { - const query2 = this.#client.getQueryCache().build(this.#client, options); - const result2 = this.createResult(query2, options); - if (shouldAssignObserverCurrentProperties(this, result2)) { - this.#currentResult = result2; - this.#currentResultOptions = this.options; - this.#currentResultState = this.#currentQuery.state; - } - return result2; - } - getCurrentResult() { - return this.#currentResult; - } - trackResult(result2, onPropTracked) { - return new Proxy(result2, { - get: (target2, key2) => { - this.trackProp(key2); - onPropTracked?.(key2); - if (key2 === "promise") { - this.trackProp("data"); - if (!this.options.experimental_prefetchInRender && this.#currentThenable.status === "pending") { - this.#currentThenable.reject( - new Error( - "experimental_prefetchInRender feature flag is not enabled" - ) - ); - } - } - return Reflect.get(target2, key2); - } - }); - } - trackProp(key2) { - this.#trackedProps.add(key2); - } - getCurrentQuery() { - return this.#currentQuery; - } - refetch({ ...options } = {}) { - return this.fetch({ - ...options - }); - } - fetchOptimistic(options) { - const defaultedOptions = this.#client.defaultQueryOptions(options); - const query2 = this.#client.getQueryCache().build(this.#client, defaultedOptions); - return query2.fetch().then(() => this.createResult(query2, defaultedOptions)); - } - fetch(fetchOptions) { - return this.#executeFetch({ - ...fetchOptions, - cancelRefetch: fetchOptions.cancelRefetch ?? true - }).then(() => { - this.updateResult(); - return this.#currentResult; - }); - } - #executeFetch(fetchOptions) { - this.#updateQuery(); - let promise = this.#currentQuery.fetch( - this.options, - fetchOptions - ); - if (!fetchOptions?.throwOnError) { - promise = promise.catch(noop$2); - } - return promise; - } - #updateStaleTimeout() { - this.#clearStaleTimeout(); - const staleTime = resolveStaleTime( - this.options.staleTime, - this.#currentQuery - ); - if (isServer || this.#currentResult.isStale || !isValidTimeout(staleTime)) { - return; - } - const time2 = timeUntilStale(this.#currentResult.dataUpdatedAt, staleTime); - const timeout = time2 + 1; - this.#staleTimeoutId = timeoutManager.setTimeout(() => { - if (!this.#currentResult.isStale) { - this.updateResult(); - } - }, timeout); - } - #computeRefetchInterval() { - return (typeof this.options.refetchInterval === "function" ? this.options.refetchInterval(this.#currentQuery) : this.options.refetchInterval) ?? false; - } - #updateRefetchInterval(nextInterval) { - this.#clearRefetchInterval(); - this.#currentRefetchInterval = nextInterval; - if (isServer || resolveEnabled(this.options.enabled, this.#currentQuery) === false || !isValidTimeout(this.#currentRefetchInterval) || this.#currentRefetchInterval === 0) { - return; - } - this.#refetchIntervalId = timeoutManager.setInterval(() => { - if (this.options.refetchIntervalInBackground || focusManager.isFocused()) { - this.#executeFetch(); - } - }, this.#currentRefetchInterval); - } - #updateTimers() { - this.#updateStaleTimeout(); - this.#updateRefetchInterval(this.#computeRefetchInterval()); - } - #clearStaleTimeout() { - if (this.#staleTimeoutId) { - timeoutManager.clearTimeout(this.#staleTimeoutId); - this.#staleTimeoutId = void 0; - } - } - #clearRefetchInterval() { - if (this.#refetchIntervalId) { - timeoutManager.clearInterval(this.#refetchIntervalId); - this.#refetchIntervalId = void 0; - } - } - createResult(query2, options) { - const prevQuery = this.#currentQuery; - const prevOptions = this.options; - const prevResult = this.#currentResult; - const prevResultState = this.#currentResultState; - const prevResultOptions = this.#currentResultOptions; - const queryChange = query2 !== prevQuery; - const queryInitialState = queryChange ? query2.state : this.#currentQueryInitialState; - const { state } = query2; - let newState = { ...state }; - let isPlaceholderData = false; - let data2; - if (options._optimisticResults) { - const mounted = this.hasListeners(); - const fetchOnMount = !mounted && shouldFetchOnMount(query2, options); - const fetchOptionally = mounted && shouldFetchOptionally(query2, prevQuery, options, prevOptions); - if (fetchOnMount || fetchOptionally) { - newState = { - ...newState, - ...fetchState(state.data, query2.options) - }; - } - if (options._optimisticResults === "isRestoring") { - newState.fetchStatus = "idle"; - } - } - let { error: error2, errorUpdatedAt, status } = newState; - data2 = newState.data; - let skipSelect = false; - if (options.placeholderData !== void 0 && data2 === void 0 && status === "pending") { - let placeholderData; - if (prevResult?.isPlaceholderData && options.placeholderData === prevResultOptions?.placeholderData) { - placeholderData = prevResult.data; - skipSelect = true; - } else { - placeholderData = typeof options.placeholderData === "function" ? options.placeholderData( - this.#lastQueryWithDefinedData?.state.data, - this.#lastQueryWithDefinedData - ) : options.placeholderData; - } - if (placeholderData !== void 0) { - status = "success"; - data2 = replaceData( - prevResult?.data, - placeholderData, - options - ); - isPlaceholderData = true; - } - } - if (options.select && data2 !== void 0 && !skipSelect) { - if (prevResult && data2 === prevResultState?.data && options.select === this.#selectFn) { - data2 = this.#selectResult; - } else { - try { - this.#selectFn = options.select; - data2 = options.select(data2); - data2 = replaceData(prevResult?.data, data2, options); - this.#selectResult = data2; - this.#selectError = null; - } catch (selectError) { - this.#selectError = selectError; - } - } - } - if (this.#selectError) { - error2 = this.#selectError; - data2 = this.#selectResult; - errorUpdatedAt = Date.now(); - status = "error"; - } - const isFetching = newState.fetchStatus === "fetching"; - const isPending = status === "pending"; - const isError = status === "error"; - const isLoading = isPending && isFetching; - const hasData = data2 !== void 0; - const result2 = { - status, - fetchStatus: newState.fetchStatus, - isPending, - isSuccess: status === "success", - isError, - isInitialLoading: isLoading, - isLoading, - data: data2, - dataUpdatedAt: newState.dataUpdatedAt, - error: error2, - errorUpdatedAt, - failureCount: newState.fetchFailureCount, - failureReason: newState.fetchFailureReason, - errorUpdateCount: newState.errorUpdateCount, - isFetched: newState.dataUpdateCount > 0 || newState.errorUpdateCount > 0, - isFetchedAfterMount: newState.dataUpdateCount > queryInitialState.dataUpdateCount || newState.errorUpdateCount > queryInitialState.errorUpdateCount, - isFetching, - isRefetching: isFetching && !isPending, - isLoadingError: isError && !hasData, - isPaused: newState.fetchStatus === "paused", - isPlaceholderData, - isRefetchError: isError && hasData, - isStale: isStale(query2, options), - refetch: this.refetch, - promise: this.#currentThenable, - isEnabled: resolveEnabled(options.enabled, query2) !== false - }; - const nextResult = result2; - if (this.options.experimental_prefetchInRender) { - const finalizeThenableIfPossible = (thenable) => { - if (nextResult.status === "error") { - thenable.reject(nextResult.error); - } else if (nextResult.data !== void 0) { - thenable.resolve(nextResult.data); - } - }; - const recreateThenable = () => { - const pending = this.#currentThenable = nextResult.promise = pendingThenable(); - finalizeThenableIfPossible(pending); - }; - const prevThenable = this.#currentThenable; - switch (prevThenable.status) { - case "pending": - if (query2.queryHash === prevQuery.queryHash) { - finalizeThenableIfPossible(prevThenable); - } - break; - case "fulfilled": - if (nextResult.status === "error" || nextResult.data !== prevThenable.value) { - recreateThenable(); - } - break; - case "rejected": - if (nextResult.status !== "error" || nextResult.error !== prevThenable.reason) { - recreateThenable(); - } - break; - } - } - return nextResult; - } - updateResult() { - const prevResult = this.#currentResult; - const nextResult = this.createResult(this.#currentQuery, this.options); - this.#currentResultState = this.#currentQuery.state; - this.#currentResultOptions = this.options; - if (this.#currentResultState.data !== void 0) { - this.#lastQueryWithDefinedData = this.#currentQuery; - } - if (shallowEqualObjects(nextResult, prevResult)) { - return; - } - this.#currentResult = nextResult; - const shouldNotifyListeners = () => { - if (!prevResult) { - return true; - } - const { notifyOnChangeProps } = this.options; - const notifyOnChangePropsValue = typeof notifyOnChangeProps === "function" ? notifyOnChangeProps() : notifyOnChangeProps; - if (notifyOnChangePropsValue === "all" || !notifyOnChangePropsValue && !this.#trackedProps.size) { - return true; - } - const includedProps = new Set( - notifyOnChangePropsValue ?? this.#trackedProps - ); - if (this.options.throwOnError) { - includedProps.add("error"); - } - return Object.keys(this.#currentResult).some((key2) => { - const typedKey = key2; - const changed = this.#currentResult[typedKey] !== prevResult[typedKey]; - return changed && includedProps.has(typedKey); - }); - }; - this.#notify({ listeners: shouldNotifyListeners() }); - } - #updateQuery() { - const query2 = this.#client.getQueryCache().build(this.#client, this.options); - if (query2 === this.#currentQuery) { - return; - } - const prevQuery = this.#currentQuery; - this.#currentQuery = query2; - this.#currentQueryInitialState = query2.state; - if (this.hasListeners()) { - prevQuery?.removeObserver(this); - query2.addObserver(this); - } - } - onQueryUpdate() { - this.updateResult(); - if (this.hasListeners()) { - this.#updateTimers(); - } - } - #notify(notifyOptions) { - notifyManager.batch(() => { - if (notifyOptions.listeners) { - this.listeners.forEach((listener) => { - listener(this.#currentResult); - }); - } - this.#client.getQueryCache().notify({ - query: this.#currentQuery, - type: "observerResultsUpdated" - }); - }); - } -}; -function shouldLoadOnMount(query2, options) { - return resolveEnabled(options.enabled, query2) !== false && query2.state.data === void 0 && !(query2.state.status === "error" && options.retryOnMount === false); -} -function shouldFetchOnMount(query2, options) { - return shouldLoadOnMount(query2, options) || query2.state.data !== void 0 && shouldFetchOn(query2, options, options.refetchOnMount); -} -function shouldFetchOn(query2, options, field2) { - if (resolveEnabled(options.enabled, query2) !== false && resolveStaleTime(options.staleTime, query2) !== "static") { - const value2 = typeof field2 === "function" ? field2(query2) : field2; - return value2 === "always" || value2 !== false && isStale(query2, options); - } - return false; -} -function shouldFetchOptionally(query2, prevQuery, options, prevOptions) { - return (query2 !== prevQuery || resolveEnabled(prevOptions.enabled, query2) === false) && (!options.suspense || query2.state.status !== "error") && isStale(query2, options); -} -function isStale(query2, options) { - return resolveEnabled(options.enabled, query2) !== false && query2.isStaleByTime(resolveStaleTime(options.staleTime, query2)); -} -function shouldAssignObserverCurrentProperties(observer, optimisticResult) { - if (!shallowEqualObjects(observer.getCurrentResult(), optimisticResult)) { - return true; - } - return false; -} -function infiniteQueryBehavior(pages) { - return { - onFetch: (context, query2) => { - const options = context.options; - const direction = context.fetchOptions?.meta?.fetchMore?.direction; - const oldPages = context.state.data?.pages || []; - const oldPageParams = context.state.data?.pageParams || []; - let result2 = { pages: [], pageParams: [] }; - let currentPage = 0; - const fetchFn = async () => { - let cancelled = false; - const addSignalProperty = (object2) => { - Object.defineProperty(object2, "signal", { - enumerable: true, - get: () => { - if (context.signal.aborted) { - cancelled = true; - } else { - context.signal.addEventListener("abort", () => { - cancelled = true; - }); - } - return context.signal; - } - }); - }; - const queryFn = ensureQueryFn(context.options, context.fetchOptions); - const fetchPage = async (data2, param, previous) => { - if (cancelled) { - return Promise.reject(); - } - if (param == null && data2.pages.length) { - return Promise.resolve(data2); - } - const createQueryFnContext = () => { - const queryFnContext2 = { - client: context.client, - queryKey: context.queryKey, - pageParam: param, - direction: previous ? "backward" : "forward", - meta: context.options.meta - }; - addSignalProperty(queryFnContext2); - return queryFnContext2; - }; - const queryFnContext = createQueryFnContext(); - const page = await queryFn(queryFnContext); - const { maxPages } = context.options; - const addTo = previous ? addToStart : addToEnd; - return { - pages: addTo(data2.pages, page, maxPages), - pageParams: addTo(data2.pageParams, param, maxPages) - }; - }; - if (direction && oldPages.length) { - const previous = direction === "backward"; - const pageParamFn = previous ? getPreviousPageParam : getNextPageParam; - const oldData = { - pages: oldPages, - pageParams: oldPageParams - }; - const param = pageParamFn(options, oldData); - result2 = await fetchPage(oldData, param, previous); - } else { - const remainingPages = pages ?? oldPages.length; - do { - const param = currentPage === 0 ? oldPageParams[0] ?? options.initialPageParam : getNextPageParam(options, result2); - if (currentPage > 0 && param == null) { - break; - } - result2 = await fetchPage(result2, param); - currentPage++; - } while (currentPage < remainingPages); - } - return result2; - }; - if (context.options.persister) { - context.fetchFn = () => { - return context.options.persister?.( - fetchFn, - { - client: context.client, - queryKey: context.queryKey, - meta: context.options.meta, - signal: context.signal - }, - query2 - ); - }; - } else { - context.fetchFn = fetchFn; - } - } - }; -} -function getNextPageParam(options, { pages, pageParams }) { - const lastIndex = pages.length - 1; - return pages.length > 0 ? options.getNextPageParam( - pages[lastIndex], - pages, - pageParams[lastIndex], - pageParams - ) : void 0; -} -function getPreviousPageParam(options, { pages, pageParams }) { - return pages.length > 0 ? options.getPreviousPageParam?.(pages[0], pages, pageParams[0], pageParams) : void 0; -} -function hasNextPage(options, data2) { - if (!data2) return false; - return getNextPageParam(options, data2) != null; -} -function hasPreviousPage(options, data2) { - if (!data2 || !options.getPreviousPageParam) return false; - return getPreviousPageParam(options, data2) != null; -} -var InfiniteQueryObserver = class extends QueryObserver { - constructor(client2, options) { - super(client2, options); - } - bindMethods() { - super.bindMethods(); - this.fetchNextPage = this.fetchNextPage.bind(this); - this.fetchPreviousPage = this.fetchPreviousPage.bind(this); - } - setOptions(options) { - super.setOptions({ - ...options, - behavior: infiniteQueryBehavior() - }); - } - getOptimisticResult(options) { - options.behavior = infiniteQueryBehavior(); - return super.getOptimisticResult(options); - } - fetchNextPage(options) { - return this.fetch({ - ...options, - meta: { - fetchMore: { direction: "forward" } - } - }); - } - fetchPreviousPage(options) { - return this.fetch({ - ...options, - meta: { - fetchMore: { direction: "backward" } - } - }); - } - createResult(query2, options) { - const { state } = query2; - const parentResult = super.createResult(query2, options); - const { isFetching, isRefetching, isError, isRefetchError } = parentResult; - const fetchDirection = state.fetchMeta?.fetchMore?.direction; - const isFetchNextPageError = isError && fetchDirection === "forward"; - const isFetchingNextPage = isFetching && fetchDirection === "forward"; - const isFetchPreviousPageError = isError && fetchDirection === "backward"; - const isFetchingPreviousPage = isFetching && fetchDirection === "backward"; - const result2 = { - ...parentResult, - fetchNextPage: this.fetchNextPage, - fetchPreviousPage: this.fetchPreviousPage, - hasNextPage: hasNextPage(options, state.data), - hasPreviousPage: hasPreviousPage(options, state.data), - isFetchNextPageError, - isFetchingNextPage, - isFetchPreviousPageError, - isFetchingPreviousPage, - isRefetchError: isRefetchError && !isFetchNextPageError && !isFetchPreviousPageError, - isRefetching: isRefetching && !isFetchingNextPage && !isFetchingPreviousPage - }; - return result2; - } -}; -var Mutation = class extends Removable { - #client; - #observers; - #mutationCache; - #retryer; - constructor(config2) { - super(); - this.#client = config2.client; - this.mutationId = config2.mutationId; - this.#mutationCache = config2.mutationCache; - this.#observers = []; - this.state = config2.state || getDefaultState(); - this.setOptions(config2.options); - this.scheduleGc(); - } - setOptions(options) { - this.options = options; - this.updateGcTime(this.options.gcTime); - } - get meta() { - return this.options.meta; - } - addObserver(observer) { - if (!this.#observers.includes(observer)) { - this.#observers.push(observer); - this.clearGcTimeout(); - this.#mutationCache.notify({ - type: "observerAdded", - mutation: this, - observer - }); - } - } - removeObserver(observer) { - this.#observers = this.#observers.filter((x2) => x2 !== observer); - this.scheduleGc(); - this.#mutationCache.notify({ - type: "observerRemoved", - mutation: this, - observer - }); - } - optionalRemove() { - if (!this.#observers.length) { - if (this.state.status === "pending") { - this.scheduleGc(); - } else { - this.#mutationCache.remove(this); - } - } - } - continue() { - return this.#retryer?.continue() ?? // continuing a mutation assumes that variables are set, mutation must have been dehydrated before - this.execute(this.state.variables); - } - async execute(variables) { - const onContinue = () => { - this.#dispatch({ type: "continue" }); - }; - const mutationFnContext = { - client: this.#client, - meta: this.options.meta, - mutationKey: this.options.mutationKey - }; - this.#retryer = createRetryer({ - fn: () => { - if (!this.options.mutationFn) { - return Promise.reject(new Error("No mutationFn found")); - } - return this.options.mutationFn(variables, mutationFnContext); - }, - onFail: (failureCount, error2) => { - this.#dispatch({ type: "failed", failureCount, error: error2 }); - }, - onPause: () => { - this.#dispatch({ type: "pause" }); - }, - onContinue, - retry: this.options.retry ?? 0, - retryDelay: this.options.retryDelay, - networkMode: this.options.networkMode, - canRun: () => this.#mutationCache.canRun(this) - }); - const restored = this.state.status === "pending"; - const isPaused = !this.#retryer.canStart(); - try { - if (restored) { - onContinue(); - } else { - this.#dispatch({ type: "pending", variables, isPaused }); - await this.#mutationCache.config.onMutate?.( - variables, - this, - mutationFnContext - ); - const context = await this.options.onMutate?.( - variables, - mutationFnContext - ); - if (context !== this.state.context) { - this.#dispatch({ - type: "pending", - context, - variables, - isPaused - }); - } - } - const data2 = await this.#retryer.start(); - await this.#mutationCache.config.onSuccess?.( - data2, - variables, - this.state.context, - this, - mutationFnContext - ); - await this.options.onSuccess?.( - data2, - variables, - this.state.context, - mutationFnContext - ); - await this.#mutationCache.config.onSettled?.( - data2, - null, - this.state.variables, - this.state.context, - this, - mutationFnContext - ); - await this.options.onSettled?.( - data2, - null, - variables, - this.state.context, - mutationFnContext - ); - this.#dispatch({ type: "success", data: data2 }); - return data2; - } catch (error2) { - try { - await this.#mutationCache.config.onError?.( - error2, - variables, - this.state.context, - this, - mutationFnContext - ); - await this.options.onError?.( - error2, - variables, - this.state.context, - mutationFnContext - ); - await this.#mutationCache.config.onSettled?.( - void 0, - error2, - this.state.variables, - this.state.context, - this, - mutationFnContext - ); - await this.options.onSettled?.( - void 0, - error2, - variables, - this.state.context, - mutationFnContext - ); - throw error2; - } finally { - this.#dispatch({ type: "error", error: error2 }); - } - } finally { - this.#mutationCache.runNext(this); - } - } - #dispatch(action2) { - const reducer = (state) => { - switch (action2.type) { - case "failed": - return { - ...state, - failureCount: action2.failureCount, - failureReason: action2.error - }; - case "pause": - return { - ...state, - isPaused: true - }; - case "continue": - return { - ...state, - isPaused: false - }; - case "pending": - return { - ...state, - context: action2.context, - data: void 0, - failureCount: 0, - failureReason: null, - error: null, - isPaused: action2.isPaused, - status: "pending", - variables: action2.variables, - submittedAt: Date.now() - }; - case "success": - return { - ...state, - data: action2.data, - failureCount: 0, - failureReason: null, - error: null, - status: "success", - isPaused: false - }; - case "error": - return { - ...state, - data: void 0, - error: action2.error, - failureCount: state.failureCount + 1, - failureReason: action2.error, - isPaused: false, - status: "error" - }; - } - }; - this.state = reducer(this.state); - notifyManager.batch(() => { - this.#observers.forEach((observer) => { - observer.onMutationUpdate(action2); - }); - this.#mutationCache.notify({ - mutation: this, - type: "updated", - action: action2 - }); - }); - } -}; -function getDefaultState() { - return { - context: void 0, - data: void 0, - error: null, - failureCount: 0, - failureReason: null, - isPaused: false, - status: "idle", - variables: void 0, - submittedAt: 0 - }; -} -var MutationCache = class extends Subscribable { - constructor(config2 = {}) { - super(); - this.config = config2; - this.#mutations = /* @__PURE__ */ new Set(); - this.#scopes = /* @__PURE__ */ new Map(); - this.#mutationId = 0; - } - #mutations; - #scopes; - #mutationId; - build(client2, options, state) { - const mutation = new Mutation({ - client: client2, - mutationCache: this, - mutationId: ++this.#mutationId, - options: client2.defaultMutationOptions(options), - state - }); - this.add(mutation); - return mutation; - } - add(mutation) { - this.#mutations.add(mutation); - const scope = scopeFor(mutation); - if (typeof scope === "string") { - const scopedMutations = this.#scopes.get(scope); - if (scopedMutations) { - scopedMutations.push(mutation); - } else { - this.#scopes.set(scope, [mutation]); - } - } - this.notify({ type: "added", mutation }); - } - remove(mutation) { - if (this.#mutations.delete(mutation)) { - const scope = scopeFor(mutation); - if (typeof scope === "string") { - const scopedMutations = this.#scopes.get(scope); - if (scopedMutations) { - if (scopedMutations.length > 1) { - const index = scopedMutations.indexOf(mutation); - if (index !== -1) { - scopedMutations.splice(index, 1); - } - } else if (scopedMutations[0] === mutation) { - this.#scopes.delete(scope); - } - } - } - } - this.notify({ type: "removed", mutation }); - } - canRun(mutation) { - const scope = scopeFor(mutation); - if (typeof scope === "string") { - const mutationsWithSameScope = this.#scopes.get(scope); - const firstPendingMutation = mutationsWithSameScope?.find( - (m2) => m2.state.status === "pending" - ); - return !firstPendingMutation || firstPendingMutation === mutation; - } else { - return true; - } - } - runNext(mutation) { - const scope = scopeFor(mutation); - if (typeof scope === "string") { - const foundMutation = this.#scopes.get(scope)?.find((m2) => m2 !== mutation && m2.state.isPaused); - return foundMutation?.continue() ?? Promise.resolve(); - } else { - return Promise.resolve(); - } - } - clear() { - notifyManager.batch(() => { - this.#mutations.forEach((mutation) => { - this.notify({ type: "removed", mutation }); - }); - this.#mutations.clear(); - this.#scopes.clear(); - }); - } - getAll() { - return Array.from(this.#mutations); - } - find(filters) { - const defaultedFilters = { exact: true, ...filters }; - return this.getAll().find( - (mutation) => matchMutation(defaultedFilters, mutation) - ); - } - findAll(filters = {}) { - return this.getAll().filter((mutation) => matchMutation(filters, mutation)); - } - notify(event) { - notifyManager.batch(() => { - this.listeners.forEach((listener) => { - listener(event); - }); - }); - } - resumePausedMutations() { - const pausedMutations = this.getAll().filter((x2) => x2.state.isPaused); - return notifyManager.batch( - () => Promise.all( - pausedMutations.map((mutation) => mutation.continue().catch(noop$2)) - ) - ); - } -}; -function scopeFor(mutation) { - return mutation.options.scope?.id; -} -var MutationObserver$1 = class MutationObserver2 extends Subscribable { - #client; - #currentResult = void 0; - #currentMutation; - #mutateOptions; - constructor(client2, options) { - super(); - this.#client = client2; - this.setOptions(options); - this.bindMethods(); - this.#updateResult(); - } - bindMethods() { - this.mutate = this.mutate.bind(this); - this.reset = this.reset.bind(this); - } - setOptions(options) { - const prevOptions = this.options; - this.options = this.#client.defaultMutationOptions(options); - if (!shallowEqualObjects(this.options, prevOptions)) { - this.#client.getMutationCache().notify({ - type: "observerOptionsUpdated", - mutation: this.#currentMutation, - observer: this - }); - } - if (prevOptions?.mutationKey && this.options.mutationKey && hashKey(prevOptions.mutationKey) !== hashKey(this.options.mutationKey)) { - this.reset(); - } else if (this.#currentMutation?.state.status === "pending") { - this.#currentMutation.setOptions(this.options); - } - } - onUnsubscribe() { - if (!this.hasListeners()) { - this.#currentMutation?.removeObserver(this); - } - } - onMutationUpdate(action2) { - this.#updateResult(); - this.#notify(action2); - } - getCurrentResult() { - return this.#currentResult; - } - reset() { - this.#currentMutation?.removeObserver(this); - this.#currentMutation = void 0; - this.#updateResult(); - this.#notify(); - } - mutate(variables, options) { - this.#mutateOptions = options; - this.#currentMutation?.removeObserver(this); - this.#currentMutation = this.#client.getMutationCache().build(this.#client, this.options); - this.#currentMutation.addObserver(this); - return this.#currentMutation.execute(variables); - } - #updateResult() { - const state = this.#currentMutation?.state ?? getDefaultState(); - this.#currentResult = { - ...state, - isPending: state.status === "pending", - isSuccess: state.status === "success", - isError: state.status === "error", - isIdle: state.status === "idle", - mutate: this.mutate, - reset: this.reset - }; - } - #notify(action2) { - notifyManager.batch(() => { - if (this.#mutateOptions && this.hasListeners()) { - const variables = this.#currentResult.variables; - const onMutateResult = this.#currentResult.context; - const context = { - client: this.#client, - meta: this.options.meta, - mutationKey: this.options.mutationKey - }; - if (action2?.type === "success") { - this.#mutateOptions.onSuccess?.( - action2.data, - variables, - onMutateResult, - context - ); - this.#mutateOptions.onSettled?.( - action2.data, - null, - variables, - onMutateResult, - context - ); - } else if (action2?.type === "error") { - this.#mutateOptions.onError?.( - action2.error, - variables, - onMutateResult, - context - ); - this.#mutateOptions.onSettled?.( - void 0, - action2.error, - variables, - onMutateResult, - context - ); - } - } - this.listeners.forEach((listener) => { - listener(this.#currentResult); - }); - }); - } -}; -var QueryCache = class extends Subscribable { - constructor(config2 = {}) { - super(); - this.config = config2; - this.#queries = /* @__PURE__ */ new Map(); - } - #queries; - build(client2, options, state) { - const queryKey = options.queryKey; - const queryHash = options.queryHash ?? hashQueryKeyByOptions(queryKey, options); - let query2 = this.get(queryHash); - if (!query2) { - query2 = new Query({ - client: client2, - queryKey, - queryHash, - options: client2.defaultQueryOptions(options), - state, - defaultOptions: client2.getQueryDefaults(queryKey) - }); - this.add(query2); - } - return query2; - } - add(query2) { - if (!this.#queries.has(query2.queryHash)) { - this.#queries.set(query2.queryHash, query2); - this.notify({ - type: "added", - query: query2 - }); - } - } - remove(query2) { - const queryInMap = this.#queries.get(query2.queryHash); - if (queryInMap) { - query2.destroy(); - if (queryInMap === query2) { - this.#queries.delete(query2.queryHash); - } - this.notify({ type: "removed", query: query2 }); - } - } - clear() { - notifyManager.batch(() => { - this.getAll().forEach((query2) => { - this.remove(query2); - }); - }); - } - get(queryHash) { - return this.#queries.get(queryHash); - } - getAll() { - return [...this.#queries.values()]; - } - find(filters) { - const defaultedFilters = { exact: true, ...filters }; - return this.getAll().find( - (query2) => matchQuery(defaultedFilters, query2) - ); - } - findAll(filters = {}) { - const queries = this.getAll(); - return Object.keys(filters).length > 0 ? queries.filter((query2) => matchQuery(filters, query2)) : queries; - } - notify(event) { - notifyManager.batch(() => { - this.listeners.forEach((listener) => { - listener(event); - }); - }); - } - onFocus() { - notifyManager.batch(() => { - this.getAll().forEach((query2) => { - query2.onFocus(); - }); - }); - } - onOnline() { - notifyManager.batch(() => { - this.getAll().forEach((query2) => { - query2.onOnline(); - }); - }); - } -}; -var QueryClient = class { - #queryCache; - #mutationCache; - #defaultOptions; - #queryDefaults; - #mutationDefaults; - #mountCount; - #unsubscribeFocus; - #unsubscribeOnline; - constructor(config2 = {}) { - this.#queryCache = config2.queryCache || new QueryCache(); - this.#mutationCache = config2.mutationCache || new MutationCache(); - this.#defaultOptions = config2.defaultOptions || {}; - this.#queryDefaults = /* @__PURE__ */ new Map(); - this.#mutationDefaults = /* @__PURE__ */ new Map(); - this.#mountCount = 0; - } - mount() { - this.#mountCount++; - if (this.#mountCount !== 1) return; - this.#unsubscribeFocus = focusManager.subscribe(async (focused) => { - if (focused) { - await this.resumePausedMutations(); - this.#queryCache.onFocus(); - } - }); - this.#unsubscribeOnline = onlineManager.subscribe(async (online) => { - if (online) { - await this.resumePausedMutations(); - this.#queryCache.onOnline(); - } - }); - } - unmount() { - this.#mountCount--; - if (this.#mountCount !== 0) return; - this.#unsubscribeFocus?.(); - this.#unsubscribeFocus = void 0; - this.#unsubscribeOnline?.(); - this.#unsubscribeOnline = void 0; - } - isFetching(filters) { - return this.#queryCache.findAll({ ...filters, fetchStatus: "fetching" }).length; - } - isMutating(filters) { - return this.#mutationCache.findAll({ ...filters, status: "pending" }).length; - } - /** - * Imperative (non-reactive) way to retrieve data for a QueryKey. - * Should only be used in callbacks or functions where reading the latest data is necessary, e.g. for optimistic updates. - * - * Hint: Do not use this function inside a component, because it won't receive updates. - * Use `useQuery` to create a `QueryObserver` that subscribes to changes. - */ - getQueryData(queryKey) { - const options = this.defaultQueryOptions({ queryKey }); - return this.#queryCache.get(options.queryHash)?.state.data; - } - ensureQueryData(options) { - const defaultedOptions = this.defaultQueryOptions(options); - const query2 = this.#queryCache.build(this, defaultedOptions); - const cachedData = query2.state.data; - if (cachedData === void 0) { - return this.fetchQuery(options); - } - if (options.revalidateIfStale && query2.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query2))) { - void this.prefetchQuery(defaultedOptions); - } - return Promise.resolve(cachedData); - } - getQueriesData(filters) { - return this.#queryCache.findAll(filters).map(({ queryKey, state }) => { - const data2 = state.data; - return [queryKey, data2]; - }); - } - setQueryData(queryKey, updater, options) { - const defaultedOptions = this.defaultQueryOptions({ queryKey }); - const query2 = this.#queryCache.get( - defaultedOptions.queryHash - ); - const prevData = query2?.state.data; - const data2 = functionalUpdate$1(updater, prevData); - if (data2 === void 0) { - return void 0; - } - return this.#queryCache.build(this, defaultedOptions).setData(data2, { ...options, manual: true }); - } - setQueriesData(filters, updater, options) { - return notifyManager.batch( - () => this.#queryCache.findAll(filters).map(({ queryKey }) => [ - queryKey, - this.setQueryData(queryKey, updater, options) - ]) - ); - } - getQueryState(queryKey) { - const options = this.defaultQueryOptions({ queryKey }); - return this.#queryCache.get( - options.queryHash - )?.state; - } - removeQueries(filters) { - const queryCache = this.#queryCache; - notifyManager.batch(() => { - queryCache.findAll(filters).forEach((query2) => { - queryCache.remove(query2); - }); - }); - } - resetQueries(filters, options) { - const queryCache = this.#queryCache; - return notifyManager.batch(() => { - queryCache.findAll(filters).forEach((query2) => { - query2.reset(); - }); - return this.refetchQueries( - { - type: "active", - ...filters - }, - options - ); - }); - } - cancelQueries(filters, cancelOptions = {}) { - const defaultedCancelOptions = { revert: true, ...cancelOptions }; - const promises = notifyManager.batch( - () => this.#queryCache.findAll(filters).map((query2) => query2.cancel(defaultedCancelOptions)) - ); - return Promise.all(promises).then(noop$2).catch(noop$2); - } - invalidateQueries(filters, options = {}) { - return notifyManager.batch(() => { - this.#queryCache.findAll(filters).forEach((query2) => { - query2.invalidate(); - }); - if (filters?.refetchType === "none") { - return Promise.resolve(); - } - return this.refetchQueries( - { - ...filters, - type: filters?.refetchType ?? filters?.type ?? "active" - }, - options - ); - }); - } - refetchQueries(filters, options = {}) { - const fetchOptions = { - ...options, - cancelRefetch: options.cancelRefetch ?? true - }; - const promises = notifyManager.batch( - () => this.#queryCache.findAll(filters).filter((query2) => !query2.isDisabled() && !query2.isStatic()).map((query2) => { - let promise = query2.fetch(void 0, fetchOptions); - if (!fetchOptions.throwOnError) { - promise = promise.catch(noop$2); - } - return query2.state.fetchStatus === "paused" ? Promise.resolve() : promise; - }) - ); - return Promise.all(promises).then(noop$2); - } - fetchQuery(options) { - const defaultedOptions = this.defaultQueryOptions(options); - if (defaultedOptions.retry === void 0) { - defaultedOptions.retry = false; - } - const query2 = this.#queryCache.build(this, defaultedOptions); - return query2.isStaleByTime( - resolveStaleTime(defaultedOptions.staleTime, query2) - ) ? query2.fetch(defaultedOptions) : Promise.resolve(query2.state.data); - } - prefetchQuery(options) { - return this.fetchQuery(options).then(noop$2).catch(noop$2); - } - fetchInfiniteQuery(options) { - options.behavior = infiniteQueryBehavior(options.pages); - return this.fetchQuery(options); - } - prefetchInfiniteQuery(options) { - return this.fetchInfiniteQuery(options).then(noop$2).catch(noop$2); - } - ensureInfiniteQueryData(options) { - options.behavior = infiniteQueryBehavior(options.pages); - return this.ensureQueryData(options); - } - resumePausedMutations() { - if (onlineManager.isOnline()) { - return this.#mutationCache.resumePausedMutations(); - } - return Promise.resolve(); - } - getQueryCache() { - return this.#queryCache; - } - getMutationCache() { - return this.#mutationCache; - } - getDefaultOptions() { - return this.#defaultOptions; - } - setDefaultOptions(options) { - this.#defaultOptions = options; - } - setQueryDefaults(queryKey, options) { - this.#queryDefaults.set(hashKey(queryKey), { - queryKey, - defaultOptions: options - }); - } - getQueryDefaults(queryKey) { - const defaults = [...this.#queryDefaults.values()]; - const result2 = {}; - defaults.forEach((queryDefault) => { - if (partialMatchKey(queryKey, queryDefault.queryKey)) { - Object.assign(result2, queryDefault.defaultOptions); - } - }); - return result2; - } - setMutationDefaults(mutationKey, options) { - this.#mutationDefaults.set(hashKey(mutationKey), { - mutationKey, - defaultOptions: options - }); - } - getMutationDefaults(mutationKey) { - const defaults = [...this.#mutationDefaults.values()]; - const result2 = {}; - defaults.forEach((queryDefault) => { - if (partialMatchKey(mutationKey, queryDefault.mutationKey)) { - Object.assign(result2, queryDefault.defaultOptions); - } - }); - return result2; - } - defaultQueryOptions(options) { - if (options._defaulted) { - return options; - } - const defaultedOptions = { - ...this.#defaultOptions.queries, - ...this.getQueryDefaults(options.queryKey), - ...options, - _defaulted: true - }; - if (!defaultedOptions.queryHash) { - defaultedOptions.queryHash = hashQueryKeyByOptions( - defaultedOptions.queryKey, - defaultedOptions - ); - } - if (defaultedOptions.refetchOnReconnect === void 0) { - defaultedOptions.refetchOnReconnect = defaultedOptions.networkMode !== "always"; - } - if (defaultedOptions.throwOnError === void 0) { - defaultedOptions.throwOnError = !!defaultedOptions.suspense; - } - if (!defaultedOptions.networkMode && defaultedOptions.persister) { - defaultedOptions.networkMode = "offlineFirst"; - } - if (defaultedOptions.queryFn === skipToken) { - defaultedOptions.enabled = false; - } - return defaultedOptions; - } - defaultMutationOptions(options) { - if (options?._defaulted) { - return options; - } - return { - ...this.#defaultOptions.mutations, - ...options?.mutationKey && this.getMutationDefaults(options.mutationKey), - ...options, - _defaulted: true - }; - } - clear() { - this.#queryCache.clear(); - this.#mutationCache.clear(); - } -}; -var react = { exports: {} }; -var react_production = {}; -var hasRequiredReact_production; -function requireReact_production() { - if (hasRequiredReact_production) return react_production; - hasRequiredReact_production = 1; - var REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = /* @__PURE__ */ Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = /* @__PURE__ */ Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = /* @__PURE__ */ Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = /* @__PURE__ */ Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = /* @__PURE__ */ Symbol.for("react.suspense"), REACT_MEMO_TYPE = /* @__PURE__ */ Symbol.for("react.memo"), REACT_LAZY_TYPE = /* @__PURE__ */ Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = /* @__PURE__ */ Symbol.for("react.activity"), MAYBE_ITERATOR_SYMBOL = Symbol.iterator; - function getIteratorFn(maybeIterable) { - if (null === maybeIterable || "object" !== typeof maybeIterable) return null; - maybeIterable = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable["@@iterator"]; - return "function" === typeof maybeIterable ? maybeIterable : null; - } - var ReactNoopUpdateQueue = { - isMounted: function() { - return false; - }, - enqueueForceUpdate: function() { - }, - enqueueReplaceState: function() { - }, - enqueueSetState: function() { - } - }, assign2 = Object.assign, emptyObject = {}; - function Component2(props, context, updater) { - this.props = props; - this.context = context; - this.refs = emptyObject; - this.updater = updater || ReactNoopUpdateQueue; - } - Component2.prototype.isReactComponent = {}; - Component2.prototype.setState = function(partialState, callback) { - if ("object" !== typeof partialState && "function" !== typeof partialState && null != partialState) - throw Error( - "takes an object of state variables to update or a function which returns an object of state variables." - ); - this.updater.enqueueSetState(this, partialState, callback, "setState"); - }; - Component2.prototype.forceUpdate = function(callback) { - this.updater.enqueueForceUpdate(this, callback, "forceUpdate"); - }; - function ComponentDummy() { - } - ComponentDummy.prototype = Component2.prototype; - function PureComponent(props, context, updater) { - this.props = props; - this.context = context; - this.refs = emptyObject; - this.updater = updater || ReactNoopUpdateQueue; - } - var pureComponentPrototype = PureComponent.prototype = new ComponentDummy(); - pureComponentPrototype.constructor = PureComponent; - assign2(pureComponentPrototype, Component2.prototype); - pureComponentPrototype.isPureReactComponent = true; - var isArrayImpl = Array.isArray; - function noop2() { - } - var ReactSharedInternals = { H: null, A: null, T: null, S: null }, hasOwnProperty2 = Object.prototype.hasOwnProperty; - function ReactElement(type, key2, props) { - var refProp = props.ref; - return { - $$typeof: REACT_ELEMENT_TYPE, - type, - key: key2, - ref: void 0 !== refProp ? refProp : null, - props - }; - } - function cloneAndReplaceKey(oldElement, newKey) { - return ReactElement(oldElement.type, newKey, oldElement.props); - } - function isValidElement(object2) { - return "object" === typeof object2 && null !== object2 && object2.$$typeof === REACT_ELEMENT_TYPE; - } - function escape2(key2) { - var escaperLookup = { "=": "=0", ":": "=2" }; - return "$" + key2.replace(/[=:]/g, function(match3) { - return escaperLookup[match3]; - }); - } - var userProvidedKeyEscapeRegex = /\/+/g; - function getElementKey(element, index) { - return "object" === typeof element && null !== element && null != element.key ? escape2("" + element.key) : index.toString(36); - } - function resolveThenable(thenable) { - switch (thenable.status) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenable.reason; - default: - switch ("string" === typeof thenable.status ? thenable.then(noop2, noop2) : (thenable.status = "pending", thenable.then( - function(fulfilledValue) { - "pending" === thenable.status && (thenable.status = "fulfilled", thenable.value = fulfilledValue); - }, - function(error2) { - "pending" === thenable.status && (thenable.status = "rejected", thenable.reason = error2); - } - )), thenable.status) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenable.reason; - } - } - throw thenable; - } - function mapIntoArray(children2, array2, escapedPrefix, nameSoFar, callback) { - var type = typeof children2; - if ("undefined" === type || "boolean" === type) children2 = null; - var invokeCallback = false; - if (null === children2) invokeCallback = true; - else - switch (type) { - case "bigint": - case "string": - case "number": - invokeCallback = true; - break; - case "object": - switch (children2.$$typeof) { - case REACT_ELEMENT_TYPE: - case REACT_PORTAL_TYPE: - invokeCallback = true; - break; - case REACT_LAZY_TYPE: - return invokeCallback = children2._init, mapIntoArray( - invokeCallback(children2._payload), - array2, - escapedPrefix, - nameSoFar, - callback - ); - } - } - if (invokeCallback) - return callback = callback(children2), invokeCallback = "" === nameSoFar ? "." + getElementKey(children2, 0) : nameSoFar, isArrayImpl(callback) ? (escapedPrefix = "", null != invokeCallback && (escapedPrefix = invokeCallback.replace(userProvidedKeyEscapeRegex, "$&/") + "/"), mapIntoArray(callback, array2, escapedPrefix, "", function(c2) { - return c2; - })) : null != callback && (isValidElement(callback) && (callback = cloneAndReplaceKey( - callback, - escapedPrefix + (null == callback.key || children2 && children2.key === callback.key ? "" : ("" + callback.key).replace( - userProvidedKeyEscapeRegex, - "$&/" - ) + "/") + invokeCallback - )), array2.push(callback)), 1; - invokeCallback = 0; - var nextNamePrefix = "" === nameSoFar ? "." : nameSoFar + ":"; - if (isArrayImpl(children2)) - for (var i4 = 0; i4 < children2.length; i4++) - nameSoFar = children2[i4], type = nextNamePrefix + getElementKey(nameSoFar, i4), invokeCallback += mapIntoArray( - nameSoFar, - array2, - escapedPrefix, - type, - callback - ); - else if (i4 = getIteratorFn(children2), "function" === typeof i4) - for (children2 = i4.call(children2), i4 = 0; !(nameSoFar = children2.next()).done; ) - nameSoFar = nameSoFar.value, type = nextNamePrefix + getElementKey(nameSoFar, i4++), invokeCallback += mapIntoArray( - nameSoFar, - array2, - escapedPrefix, - type, - callback - ); - else if ("object" === type) { - if ("function" === typeof children2.then) - return mapIntoArray( - resolveThenable(children2), - array2, - escapedPrefix, - nameSoFar, - callback - ); - array2 = String(children2); - throw Error( - "Objects are not valid as a React child (found: " + ("[object Object]" === array2 ? "object with keys {" + Object.keys(children2).join(", ") + "}" : array2) + "). If you meant to render a collection of children, use an array instead." - ); - } - return invokeCallback; - } - function mapChildren(children2, func2, context) { - if (null == children2) return children2; - var result2 = [], count2 = 0; - mapIntoArray(children2, result2, "", "", function(child) { - return func2.call(context, child, count2++); - }); - return result2; - } - function lazyInitializer(payload) { - if (-1 === payload._status) { - var ctor = payload._result; - ctor = ctor(); - ctor.then( - function(moduleObject) { - if (0 === payload._status || -1 === payload._status) - payload._status = 1, payload._result = moduleObject; - }, - function(error2) { - if (0 === payload._status || -1 === payload._status) - payload._status = 2, payload._result = error2; - } - ); - -1 === payload._status && (payload._status = 0, payload._result = ctor); - } - if (1 === payload._status) return payload._result.default; - throw payload._result; - } - var reportGlobalError = "function" === typeof reportError ? reportError : function(error2) { - if ("object" === typeof window && "function" === typeof window.ErrorEvent) { - var event = new window.ErrorEvent("error", { - bubbles: true, - cancelable: true, - message: "object" === typeof error2 && null !== error2 && "string" === typeof error2.message ? String(error2.message) : String(error2), - error: error2 - }); - if (!window.dispatchEvent(event)) return; - } else if ("object" === typeof process && "function" === typeof process.emit) { - process.emit("uncaughtException", error2); - return; - } - console.error(error2); - }, Children = { - map: mapChildren, - forEach: function(children2, forEachFunc, forEachContext) { - mapChildren( - children2, - function() { - forEachFunc.apply(this, arguments); - }, - forEachContext - ); - }, - count: function(children2) { - var n3 = 0; - mapChildren(children2, function() { - n3++; - }); - return n3; - }, - toArray: function(children2) { - return mapChildren(children2, function(child) { - return child; - }) || []; - }, - only: function(children2) { - if (!isValidElement(children2)) - throw Error( - "React.Children.only expected to receive a single React element child." - ); - return children2; - } - }; - react_production.Activity = REACT_ACTIVITY_TYPE; - react_production.Children = Children; - react_production.Component = Component2; - react_production.Fragment = REACT_FRAGMENT_TYPE; - react_production.Profiler = REACT_PROFILER_TYPE; - react_production.PureComponent = PureComponent; - react_production.StrictMode = REACT_STRICT_MODE_TYPE; - react_production.Suspense = REACT_SUSPENSE_TYPE; - react_production.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = ReactSharedInternals; - react_production.__COMPILER_RUNTIME = { - __proto__: null, - c: function(size) { - return ReactSharedInternals.H.useMemoCache(size); - } - }; - react_production.cache = function(fn3) { - return function() { - return fn3.apply(null, arguments); - }; - }; - react_production.cacheSignal = function() { - return null; - }; - react_production.cloneElement = function(element, config2, children2) { - if (null === element || void 0 === element) - throw Error( - "The argument must be a React element, but you passed " + element + "." - ); - var props = assign2({}, element.props), key2 = element.key; - if (null != config2) - for (propName in void 0 !== config2.key && (key2 = "" + config2.key), config2) - !hasOwnProperty2.call(config2, propName) || "key" === propName || "__self" === propName || "__source" === propName || "ref" === propName && void 0 === config2.ref || (props[propName] = config2[propName]); - var propName = arguments.length - 2; - if (1 === propName) props.children = children2; - else if (1 < propName) { - for (var childArray = Array(propName), i4 = 0; i4 < propName; i4++) - childArray[i4] = arguments[i4 + 2]; - props.children = childArray; - } - return ReactElement(element.type, key2, props); - }; - react_production.createContext = function(defaultValue) { - defaultValue = { - $$typeof: REACT_CONTEXT_TYPE, - _currentValue: defaultValue, - _currentValue2: defaultValue, - _threadCount: 0, - Provider: null, - Consumer: null - }; - defaultValue.Provider = defaultValue; - defaultValue.Consumer = { - $$typeof: REACT_CONSUMER_TYPE, - _context: defaultValue - }; - return defaultValue; - }; - react_production.createElement = function(type, config2, children2) { - var propName, props = {}, key2 = null; - if (null != config2) - for (propName in void 0 !== config2.key && (key2 = "" + config2.key), config2) - hasOwnProperty2.call(config2, propName) && "key" !== propName && "__self" !== propName && "__source" !== propName && (props[propName] = config2[propName]); - var childrenLength = arguments.length - 2; - if (1 === childrenLength) props.children = children2; - else if (1 < childrenLength) { - for (var childArray = Array(childrenLength), i4 = 0; i4 < childrenLength; i4++) - childArray[i4] = arguments[i4 + 2]; - props.children = childArray; - } - if (type && type.defaultProps) - for (propName in childrenLength = type.defaultProps, childrenLength) - void 0 === props[propName] && (props[propName] = childrenLength[propName]); - return ReactElement(type, key2, props); - }; - react_production.createRef = function() { - return { current: null }; - }; - react_production.forwardRef = function(render2) { - return { $$typeof: REACT_FORWARD_REF_TYPE, render: render2 }; - }; - react_production.isValidElement = isValidElement; - react_production.lazy = function(ctor) { - return { - $$typeof: REACT_LAZY_TYPE, - _payload: { _status: -1, _result: ctor }, - _init: lazyInitializer - }; - }; - react_production.memo = function(type, compare2) { - return { - $$typeof: REACT_MEMO_TYPE, - type, - compare: void 0 === compare2 ? null : compare2 - }; - }; - react_production.startTransition = function(scope) { - var prevTransition = ReactSharedInternals.T, currentTransition = {}; - ReactSharedInternals.T = currentTransition; - try { - var returnValue = scope(), onStartTransitionFinish = ReactSharedInternals.S; - null !== onStartTransitionFinish && onStartTransitionFinish(currentTransition, returnValue); - "object" === typeof returnValue && null !== returnValue && "function" === typeof returnValue.then && returnValue.then(noop2, reportGlobalError); - } catch (error2) { - reportGlobalError(error2); - } finally { - null !== prevTransition && null !== currentTransition.types && (prevTransition.types = currentTransition.types), ReactSharedInternals.T = prevTransition; - } - }; - react_production.unstable_useCacheRefresh = function() { - return ReactSharedInternals.H.useCacheRefresh(); - }; - react_production.use = function(usable) { - return ReactSharedInternals.H.use(usable); - }; - react_production.useActionState = function(action2, initialState, permalink) { - return ReactSharedInternals.H.useActionState(action2, initialState, permalink); - }; - react_production.useCallback = function(callback, deps) { - return ReactSharedInternals.H.useCallback(callback, deps); - }; - react_production.useContext = function(Context2) { - return ReactSharedInternals.H.useContext(Context2); - }; - react_production.useDebugValue = function() { - }; - react_production.useDeferredValue = function(value2, initialValue) { - return ReactSharedInternals.H.useDeferredValue(value2, initialValue); - }; - react_production.useEffect = function(create2, deps) { - return ReactSharedInternals.H.useEffect(create2, deps); - }; - react_production.useEffectEvent = function(callback) { - return ReactSharedInternals.H.useEffectEvent(callback); - }; - react_production.useId = function() { - return ReactSharedInternals.H.useId(); - }; - react_production.useImperativeHandle = function(ref2, create2, deps) { - return ReactSharedInternals.H.useImperativeHandle(ref2, create2, deps); - }; - react_production.useInsertionEffect = function(create2, deps) { - return ReactSharedInternals.H.useInsertionEffect(create2, deps); - }; - react_production.useLayoutEffect = function(create2, deps) { - return ReactSharedInternals.H.useLayoutEffect(create2, deps); - }; - react_production.useMemo = function(create2, deps) { - return ReactSharedInternals.H.useMemo(create2, deps); - }; - react_production.useOptimistic = function(passthrough, reducer) { - return ReactSharedInternals.H.useOptimistic(passthrough, reducer); - }; - react_production.useReducer = function(reducer, initialArg, init) { - return ReactSharedInternals.H.useReducer(reducer, initialArg, init); - }; - react_production.useRef = function(initialValue) { - return ReactSharedInternals.H.useRef(initialValue); - }; - react_production.useState = function(initialState) { - return ReactSharedInternals.H.useState(initialState); - }; - react_production.useSyncExternalStore = function(subscribe, getSnapshot, getServerSnapshot) { - return ReactSharedInternals.H.useSyncExternalStore( - subscribe, - getSnapshot, - getServerSnapshot - ); - }; - react_production.useTransition = function() { - return ReactSharedInternals.H.useTransition(); - }; - react_production.version = "19.2.3"; - return react_production; -} -var hasRequiredReact; -function requireReact() { - if (hasRequiredReact) return react.exports; - hasRequiredReact = 1; - { - react.exports = requireReact_production(); - } - return react.exports; -} -var reactExports = requireReact(); -const E$1 = /* @__PURE__ */ getDefaultExportFromCjs(reactExports); -const React4 = /* @__PURE__ */ _mergeNamespaces({ - __proto__: null, - default: E$1 -}, [reactExports]); -var QueryClientContext = reactExports.createContext( - void 0 -); -var useQueryClient = (queryClient2) => { - const client2 = reactExports.useContext(QueryClientContext); - if (!client2) { - throw new Error("No QueryClient set, use QueryClientProvider to set one"); - } - return client2; -}; -var QueryClientProvider = ({ - client: client2, - children: children2 -}) => { - reactExports.useEffect(() => { - client2.mount(); - return () => { - client2.unmount(); - }; - }, [client2]); - return /* @__PURE__ */ jsxRuntimeExports.jsx(QueryClientContext.Provider, { value: client2, children: children2 }); -}; -var IsRestoringContext = reactExports.createContext(false); -var useIsRestoring = () => reactExports.useContext(IsRestoringContext); -IsRestoringContext.Provider; -function createValue() { - let isReset = false; - return { - clearReset: () => { - isReset = false; - }, - reset: () => { - isReset = true; - }, - isReset: () => { - return isReset; - } - }; -} -var QueryErrorResetBoundaryContext = reactExports.createContext(createValue()); -var useQueryErrorResetBoundary = () => reactExports.useContext(QueryErrorResetBoundaryContext); -var ensurePreventErrorBoundaryRetry = (options, errorResetBoundary) => { - if (options.suspense || options.throwOnError || options.experimental_prefetchInRender) { - if (!errorResetBoundary.isReset()) { - options.retryOnMount = false; - } - } -}; -var useClearResetErrorBoundary = (errorResetBoundary) => { - reactExports.useEffect(() => { - errorResetBoundary.clearReset(); - }, [errorResetBoundary]); -}; -var getHasError = ({ - result: result2, - errorResetBoundary, - throwOnError, - query: query2, - suspense -}) => { - return result2.isError && !errorResetBoundary.isReset() && !result2.isFetching && query2 && (suspense && result2.data === void 0 || shouldThrowError(throwOnError, [result2.error, query2])); -}; -var ensureSuspenseTimers = (defaultedOptions) => { - if (defaultedOptions.suspense) { - const MIN_SUSPENSE_TIME_MS = 1e3; - const clamp2 = (value2) => value2 === "static" ? value2 : Math.max(value2 ?? MIN_SUSPENSE_TIME_MS, MIN_SUSPENSE_TIME_MS); - const originalStaleTime = defaultedOptions.staleTime; - defaultedOptions.staleTime = typeof originalStaleTime === "function" ? (...args2) => clamp2(originalStaleTime(...args2)) : clamp2(originalStaleTime); - if (typeof defaultedOptions.gcTime === "number") { - defaultedOptions.gcTime = Math.max( - defaultedOptions.gcTime, - MIN_SUSPENSE_TIME_MS - ); - } - } -}; -var willFetch = (result2, isRestoring) => result2.isLoading && result2.isFetching && !isRestoring; -var shouldSuspend = (defaultedOptions, result2) => defaultedOptions?.suspense && result2.isPending; -var fetchOptimistic = (defaultedOptions, observer, errorResetBoundary) => observer.fetchOptimistic(defaultedOptions).catch(() => { - errorResetBoundary.clearReset(); -}); -function useBaseQuery(options, Observer, queryClient2) { - const isRestoring = useIsRestoring(); - const errorResetBoundary = useQueryErrorResetBoundary(); - const client2 = useQueryClient(); - const defaultedOptions = client2.defaultQueryOptions(options); - client2.getDefaultOptions().queries?._experimental_beforeQuery?.( - defaultedOptions - ); - defaultedOptions._optimisticResults = isRestoring ? "isRestoring" : "optimistic"; - ensureSuspenseTimers(defaultedOptions); - ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary); - useClearResetErrorBoundary(errorResetBoundary); - const isNewCacheEntry = !client2.getQueryCache().get(defaultedOptions.queryHash); - const [observer] = reactExports.useState( - () => new Observer( - client2, - defaultedOptions - ) - ); - const result2 = observer.getOptimisticResult(defaultedOptions); - const shouldSubscribe = !isRestoring && options.subscribed !== false; - reactExports.useSyncExternalStore( - reactExports.useCallback( - (onStoreChange) => { - const unsubscribe = shouldSubscribe ? observer.subscribe(notifyManager.batchCalls(onStoreChange)) : noop$2; - observer.updateResult(); - return unsubscribe; - }, - [observer, shouldSubscribe] - ), - () => observer.getCurrentResult(), - () => observer.getCurrentResult() - ); - reactExports.useEffect(() => { - observer.setOptions(defaultedOptions); - }, [defaultedOptions, observer]); - if (shouldSuspend(defaultedOptions, result2)) { - throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary); - } - if (getHasError({ - result: result2, - errorResetBoundary, - throwOnError: defaultedOptions.throwOnError, - query: client2.getQueryCache().get(defaultedOptions.queryHash), - suspense: defaultedOptions.suspense - })) { - throw result2.error; - } - client2.getDefaultOptions().queries?._experimental_afterQuery?.( - defaultedOptions, - result2 - ); - if (defaultedOptions.experimental_prefetchInRender && !isServer && willFetch(result2, isRestoring)) { - const promise = isNewCacheEntry ? ( - // Fetch immediately on render in order to ensure `.promise` is resolved even if the component is unmounted - fetchOptimistic(defaultedOptions, observer, errorResetBoundary) - ) : ( - // subscribe to the "cache promise" so that we can finalize the currentThenable once data comes in - client2.getQueryCache().get(defaultedOptions.queryHash)?.promise - ); - promise?.catch(noop$2).finally(() => { - observer.updateResult(); - }); - } - return !defaultedOptions.notifyOnChangeProps ? observer.trackResult(result2) : result2; -} -function useQuery(options, queryClient2) { - return useBaseQuery(options, QueryObserver); -} -function useMutation(options, queryClient2) { - const client2 = useQueryClient(); - const [observer] = reactExports.useState( - () => new MutationObserver$1( - client2, - options - ) - ); - reactExports.useEffect(() => { - observer.setOptions(options); - }, [observer, options]); - const result2 = reactExports.useSyncExternalStore( - reactExports.useCallback( - (onStoreChange) => observer.subscribe(notifyManager.batchCalls(onStoreChange)), - [observer] - ), - () => observer.getCurrentResult(), - () => observer.getCurrentResult() - ); - const mutate = reactExports.useCallback( - (variables, mutateOptions) => { - observer.mutate(variables, mutateOptions).catch(noop$2); - }, - [observer] - ); - if (result2.error && shouldThrowError(observer.options.throwOnError, [result2.error])) { - throw result2.error; - } - return { ...result2, mutate, mutateAsync: result2.mutate }; -} -function useInfiniteQuery(options, queryClient2) { - return useBaseQuery( - options, - InfiniteQueryObserver - ); -} -const scriptRel = "modulepreload"; -const assetsURL = function(dep, importerUrl) { - return new URL(dep, importerUrl).href; -}; -const seen = {}; -const __vitePreload = function preload(baseModule, deps, importerUrl) { - let promise = Promise.resolve(); - if (deps && deps.length > 0) { - let allSettled = function(promises$2) { - return Promise.all(promises$2.map((p2) => Promise.resolve(p2).then((value$12) => ({ - status: "fulfilled", - value: value$12 - }), (reason) => ({ - status: "rejected", - reason - })))); - }; - const links2 = document.getElementsByTagName("link"); - const cspNonceMeta = document.querySelector("meta[property=csp-nonce]"); - const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce"); - promise = allSettled(deps.map((dep) => { - dep = assetsURL(dep, importerUrl); - if (dep in seen) return; - seen[dep] = true; - const isCss = dep.endsWith(".css"); - const cssSelector = isCss ? '[rel="stylesheet"]' : ""; - if (!!importerUrl) for (let i$12 = links2.length - 1; i$12 >= 0; i$12--) { - const link$12 = links2[i$12]; - if (link$12.href === dep && (!isCss || link$12.rel === "stylesheet")) return; - } - else if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) return; - const link2 = document.createElement("link"); - link2.rel = isCss ? "stylesheet" : scriptRel; - if (!isCss) link2.as = "script"; - link2.crossOrigin = ""; - link2.href = dep; - if (cspNonce) link2.setAttribute("nonce", cspNonce); - document.head.appendChild(link2); - if (isCss) return new Promise((res, rej) => { - link2.addEventListener("load", res); - link2.addEventListener("error", () => rej(/* @__PURE__ */ new Error(`Unable to preload CSS for ${dep}`))); - }); - })); - } - function handlePreloadError(err$2) { - const e$12 = new Event("vite:preloadError", { cancelable: true }); - e$12.payload = err$2; - window.dispatchEvent(e$12); - if (!e$12.defaultPrevented) throw err$2; - } - return promise.then((res) => { - for (const item2 of res || []) { - if (item2.status !== "rejected") continue; - handlePreloadError(item2.reason); - } - return baseModule().catch(handlePreloadError); - }); -}; -var ReactQueryDevtools2 = function() { - return null; -}; -var client = { exports: {} }; -var reactDomClient_production = {}; -var scheduler = { exports: {} }; -var scheduler_production = {}; -var hasRequiredScheduler_production; -function requireScheduler_production() { - if (hasRequiredScheduler_production) return scheduler_production; - hasRequiredScheduler_production = 1; - (function(exports2) { - function push2(heap2, node2) { - var index = heap2.length; - heap2.push(node2); - a: for (; 0 < index; ) { - var parentIndex = index - 1 >>> 1, parent = heap2[parentIndex]; - if (0 < compare2(parent, node2)) - heap2[parentIndex] = node2, heap2[index] = parent, index = parentIndex; - else break a; - } - } - function peek2(heap2) { - return 0 === heap2.length ? null : heap2[0]; - } - function pop2(heap2) { - if (0 === heap2.length) return null; - var first2 = heap2[0], last2 = heap2.pop(); - if (last2 !== first2) { - heap2[0] = last2; - a: for (var index = 0, length2 = heap2.length, halfLength = length2 >>> 1; index < halfLength; ) { - var leftIndex = 2 * (index + 1) - 1, left2 = heap2[leftIndex], rightIndex = leftIndex + 1, right2 = heap2[rightIndex]; - if (0 > compare2(left2, last2)) - rightIndex < length2 && 0 > compare2(right2, left2) ? (heap2[index] = right2, heap2[rightIndex] = last2, index = rightIndex) : (heap2[index] = left2, heap2[leftIndex] = last2, index = leftIndex); - else if (rightIndex < length2 && 0 > compare2(right2, last2)) - heap2[index] = right2, heap2[rightIndex] = last2, index = rightIndex; - else break a; - } - } - return first2; - } - function compare2(a2, b2) { - var diff2 = a2.sortIndex - b2.sortIndex; - return 0 !== diff2 ? diff2 : a2.id - b2.id; - } - exports2.unstable_now = void 0; - if ("object" === typeof performance && "function" === typeof performance.now) { - var localPerformance = performance; - exports2.unstable_now = function() { - return localPerformance.now(); - }; - } else { - var localDate = Date, initialTime = localDate.now(); - exports2.unstable_now = function() { - return localDate.now() - initialTime; - }; - } - var taskQueue = [], timerQueue = [], taskIdCounter = 1, currentTask = null, currentPriorityLevel = 3, isPerformingWork = false, isHostCallbackScheduled = false, isHostTimeoutScheduled = false, needsPaint = false, localSetTimeout = "function" === typeof setTimeout ? setTimeout : null, localClearTimeout = "function" === typeof clearTimeout ? clearTimeout : null, localSetImmediate = "undefined" !== typeof setImmediate ? setImmediate : null; - function advanceTimers(currentTime) { - for (var timer = peek2(timerQueue); null !== timer; ) { - if (null === timer.callback) pop2(timerQueue); - else if (timer.startTime <= currentTime) - pop2(timerQueue), timer.sortIndex = timer.expirationTime, push2(taskQueue, timer); - else break; - timer = peek2(timerQueue); - } - } - function handleTimeout(currentTime) { - isHostTimeoutScheduled = false; - advanceTimers(currentTime); - if (!isHostCallbackScheduled) - if (null !== peek2(taskQueue)) - isHostCallbackScheduled = true, isMessageLoopRunning || (isMessageLoopRunning = true, schedulePerformWorkUntilDeadline()); - else { - var firstTimer = peek2(timerQueue); - null !== firstTimer && requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); - } - } - var isMessageLoopRunning = false, taskTimeoutID = -1, frameInterval = 5, startTime = -1; - function shouldYieldToHost() { - return needsPaint ? true : exports2.unstable_now() - startTime < frameInterval ? false : true; - } - function performWorkUntilDeadline() { - needsPaint = false; - if (isMessageLoopRunning) { - var currentTime = exports2.unstable_now(); - startTime = currentTime; - var hasMoreWork = true; - try { - a: { - isHostCallbackScheduled = false; - isHostTimeoutScheduled && (isHostTimeoutScheduled = false, localClearTimeout(taskTimeoutID), taskTimeoutID = -1); - isPerformingWork = true; - var previousPriorityLevel = currentPriorityLevel; - try { - b: { - advanceTimers(currentTime); - for (currentTask = peek2(taskQueue); null !== currentTask && !(currentTask.expirationTime > currentTime && shouldYieldToHost()); ) { - var callback = currentTask.callback; - if ("function" === typeof callback) { - currentTask.callback = null; - currentPriorityLevel = currentTask.priorityLevel; - var continuationCallback = callback( - currentTask.expirationTime <= currentTime - ); - currentTime = exports2.unstable_now(); - if ("function" === typeof continuationCallback) { - currentTask.callback = continuationCallback; - advanceTimers(currentTime); - hasMoreWork = true; - break b; - } - currentTask === peek2(taskQueue) && pop2(taskQueue); - advanceTimers(currentTime); - } else pop2(taskQueue); - currentTask = peek2(taskQueue); - } - if (null !== currentTask) hasMoreWork = true; - else { - var firstTimer = peek2(timerQueue); - null !== firstTimer && requestHostTimeout( - handleTimeout, - firstTimer.startTime - currentTime - ); - hasMoreWork = false; - } - } - break a; - } finally { - currentTask = null, currentPriorityLevel = previousPriorityLevel, isPerformingWork = false; - } - hasMoreWork = void 0; - } - } finally { - hasMoreWork ? schedulePerformWorkUntilDeadline() : isMessageLoopRunning = false; - } - } - } - var schedulePerformWorkUntilDeadline; - if ("function" === typeof localSetImmediate) - schedulePerformWorkUntilDeadline = function() { - localSetImmediate(performWorkUntilDeadline); - }; - else if ("undefined" !== typeof MessageChannel) { - var channel = new MessageChannel(), port = channel.port2; - channel.port1.onmessage = performWorkUntilDeadline; - schedulePerformWorkUntilDeadline = function() { - port.postMessage(null); - }; - } else - schedulePerformWorkUntilDeadline = function() { - localSetTimeout(performWorkUntilDeadline, 0); - }; - function requestHostTimeout(callback, ms) { - taskTimeoutID = localSetTimeout(function() { - callback(exports2.unstable_now()); - }, ms); - } - exports2.unstable_IdlePriority = 5; - exports2.unstable_ImmediatePriority = 1; - exports2.unstable_LowPriority = 4; - exports2.unstable_NormalPriority = 3; - exports2.unstable_Profiling = null; - exports2.unstable_UserBlockingPriority = 2; - exports2.unstable_cancelCallback = function(task) { - task.callback = null; - }; - exports2.unstable_forceFrameRate = function(fps) { - 0 > fps || 125 < fps ? console.error( - "forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported" - ) : frameInterval = 0 < fps ? Math.floor(1e3 / fps) : 5; - }; - exports2.unstable_getCurrentPriorityLevel = function() { - return currentPriorityLevel; - }; - exports2.unstable_next = function(eventHandler2) { - switch (currentPriorityLevel) { - case 1: - case 2: - case 3: - var priorityLevel = 3; - break; - default: - priorityLevel = currentPriorityLevel; - } - var previousPriorityLevel = currentPriorityLevel; - currentPriorityLevel = priorityLevel; - try { - return eventHandler2(); - } finally { - currentPriorityLevel = previousPriorityLevel; - } - }; - exports2.unstable_requestPaint = function() { - needsPaint = true; - }; - exports2.unstable_runWithPriority = function(priorityLevel, eventHandler2) { - switch (priorityLevel) { - case 1: - case 2: - case 3: - case 4: - case 5: - break; - default: - priorityLevel = 3; - } - var previousPriorityLevel = currentPriorityLevel; - currentPriorityLevel = priorityLevel; - try { - return eventHandler2(); - } finally { - currentPriorityLevel = previousPriorityLevel; - } - }; - exports2.unstable_scheduleCallback = function(priorityLevel, callback, options) { - var currentTime = exports2.unstable_now(); - "object" === typeof options && null !== options ? (options = options.delay, options = "number" === typeof options && 0 < options ? currentTime + options : currentTime) : options = currentTime; - switch (priorityLevel) { - case 1: - var timeout = -1; - break; - case 2: - timeout = 250; - break; - case 5: - timeout = 1073741823; - break; - case 4: - timeout = 1e4; - break; - default: - timeout = 5e3; - } - timeout = options + timeout; - priorityLevel = { - id: taskIdCounter++, - callback, - priorityLevel, - startTime: options, - expirationTime: timeout, - sortIndex: -1 - }; - options > currentTime ? (priorityLevel.sortIndex = options, push2(timerQueue, priorityLevel), null === peek2(taskQueue) && priorityLevel === peek2(timerQueue) && (isHostTimeoutScheduled ? (localClearTimeout(taskTimeoutID), taskTimeoutID = -1) : isHostTimeoutScheduled = true, requestHostTimeout(handleTimeout, options - currentTime))) : (priorityLevel.sortIndex = timeout, push2(taskQueue, priorityLevel), isHostCallbackScheduled || isPerformingWork || (isHostCallbackScheduled = true, isMessageLoopRunning || (isMessageLoopRunning = true, schedulePerformWorkUntilDeadline()))); - return priorityLevel; - }; - exports2.unstable_shouldYield = shouldYieldToHost; - exports2.unstable_wrapCallback = function(callback) { - var parentPriorityLevel = currentPriorityLevel; - return function() { - var previousPriorityLevel = currentPriorityLevel; - currentPriorityLevel = parentPriorityLevel; - try { - return callback.apply(this, arguments); - } finally { - currentPriorityLevel = previousPriorityLevel; - } - }; - }; - })(scheduler_production); - return scheduler_production; -} -var hasRequiredScheduler; -function requireScheduler() { - if (hasRequiredScheduler) return scheduler.exports; - hasRequiredScheduler = 1; - { - scheduler.exports = requireScheduler_production(); - } - return scheduler.exports; -} -var reactDom = { exports: {} }; -var reactDom_production = {}; -var hasRequiredReactDom_production; -function requireReactDom_production() { - if (hasRequiredReactDom_production) return reactDom_production; - hasRequiredReactDom_production = 1; - var React = requireReact(); - function formatProdErrorMessage(code2) { - var url = "https://react.dev/errors/" + code2; - if (1 < arguments.length) { - url += "?args[]=" + encodeURIComponent(arguments[1]); - for (var i4 = 2; i4 < arguments.length; i4++) - url += "&args[]=" + encodeURIComponent(arguments[i4]); - } - return "Minified React error #" + code2 + "; visit " + url + " for the full message or use the non-minified dev environment for full errors and additional helpful warnings."; - } - function noop2() { - } - var Internals = { - d: { - f: noop2, - r: function() { - throw Error(formatProdErrorMessage(522)); - }, - D: noop2, - C: noop2, - L: noop2, - m: noop2, - X: noop2, - S: noop2, - M: noop2 - }, - p: 0, - findDOMNode: null - }, REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for("react.portal"); - function createPortal$1(children2, containerInfo, implementation) { - var key2 = 3 < arguments.length && void 0 !== arguments[3] ? arguments[3] : null; - return { - $$typeof: REACT_PORTAL_TYPE, - key: null == key2 ? null : "" + key2, - children: children2, - containerInfo, - implementation - }; - } - var ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; - function getCrossOriginStringAs(as, input2) { - if ("font" === as) return ""; - if ("string" === typeof input2) - return "use-credentials" === input2 ? input2 : ""; - } - reactDom_production.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = Internals; - reactDom_production.createPortal = function(children2, container2) { - var key2 = 2 < arguments.length && void 0 !== arguments[2] ? arguments[2] : null; - if (!container2 || 1 !== container2.nodeType && 9 !== container2.nodeType && 11 !== container2.nodeType) - throw Error(formatProdErrorMessage(299)); - return createPortal$1(children2, container2, null, key2); - }; - reactDom_production.flushSync = function(fn3) { - var previousTransition = ReactSharedInternals.T, previousUpdatePriority = Internals.p; - try { - if (ReactSharedInternals.T = null, Internals.p = 2, fn3) return fn3(); - } finally { - ReactSharedInternals.T = previousTransition, Internals.p = previousUpdatePriority, Internals.d.f(); - } - }; - reactDom_production.preconnect = function(href, options) { - "string" === typeof href && (options ? (options = options.crossOrigin, options = "string" === typeof options ? "use-credentials" === options ? options : "" : void 0) : options = null, Internals.d.C(href, options)); - }; - reactDom_production.prefetchDNS = function(href) { - "string" === typeof href && Internals.d.D(href); - }; - reactDom_production.preinit = function(href, options) { - if ("string" === typeof href && options && "string" === typeof options.as) { - var as = options.as, crossOrigin = getCrossOriginStringAs(as, options.crossOrigin), integrity = "string" === typeof options.integrity ? options.integrity : void 0, fetchPriority = "string" === typeof options.fetchPriority ? options.fetchPriority : void 0; - "style" === as ? Internals.d.S( - href, - "string" === typeof options.precedence ? options.precedence : void 0, - { - crossOrigin, - integrity, - fetchPriority - } - ) : "script" === as && Internals.d.X(href, { - crossOrigin, - integrity, - fetchPriority, - nonce: "string" === typeof options.nonce ? options.nonce : void 0 - }); - } - }; - reactDom_production.preinitModule = function(href, options) { - if ("string" === typeof href) - if ("object" === typeof options && null !== options) { - if (null == options.as || "script" === options.as) { - var crossOrigin = getCrossOriginStringAs( - options.as, - options.crossOrigin - ); - Internals.d.M(href, { - crossOrigin, - integrity: "string" === typeof options.integrity ? options.integrity : void 0, - nonce: "string" === typeof options.nonce ? options.nonce : void 0 - }); - } - } else null == options && Internals.d.M(href); - }; - reactDom_production.preload = function(href, options) { - if ("string" === typeof href && "object" === typeof options && null !== options && "string" === typeof options.as) { - var as = options.as, crossOrigin = getCrossOriginStringAs(as, options.crossOrigin); - Internals.d.L(href, as, { - crossOrigin, - integrity: "string" === typeof options.integrity ? options.integrity : void 0, - nonce: "string" === typeof options.nonce ? options.nonce : void 0, - type: "string" === typeof options.type ? options.type : void 0, - fetchPriority: "string" === typeof options.fetchPriority ? options.fetchPriority : void 0, - referrerPolicy: "string" === typeof options.referrerPolicy ? options.referrerPolicy : void 0, - imageSrcSet: "string" === typeof options.imageSrcSet ? options.imageSrcSet : void 0, - imageSizes: "string" === typeof options.imageSizes ? options.imageSizes : void 0, - media: "string" === typeof options.media ? options.media : void 0 - }); - } - }; - reactDom_production.preloadModule = function(href, options) { - if ("string" === typeof href) - if (options) { - var crossOrigin = getCrossOriginStringAs(options.as, options.crossOrigin); - Internals.d.m(href, { - as: "string" === typeof options.as && "script" !== options.as ? options.as : void 0, - crossOrigin, - integrity: "string" === typeof options.integrity ? options.integrity : void 0 - }); - } else Internals.d.m(href); - }; - reactDom_production.requestFormReset = function(form) { - Internals.d.r(form); - }; - reactDom_production.unstable_batchedUpdates = function(fn3, a2) { - return fn3(a2); - }; - reactDom_production.useFormState = function(action2, initialState, permalink) { - return ReactSharedInternals.H.useFormState(action2, initialState, permalink); - }; - reactDom_production.useFormStatus = function() { - return ReactSharedInternals.H.useHostTransitionStatus(); - }; - reactDom_production.version = "19.2.3"; - return reactDom_production; -} -var hasRequiredReactDom; -function requireReactDom() { - if (hasRequiredReactDom) return reactDom.exports; - hasRequiredReactDom = 1; - function checkDCE() { - if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === "undefined" || typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE !== "function") { - return; - } - try { - __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(checkDCE); - } catch (err2) { - console.error(err2); - } - } - { - checkDCE(); - reactDom.exports = requireReactDom_production(); - } - return reactDom.exports; -} -var hasRequiredReactDomClient_production; -function requireReactDomClient_production() { - if (hasRequiredReactDomClient_production) return reactDomClient_production; - hasRequiredReactDomClient_production = 1; - var Scheduler = requireScheduler(), React = requireReact(), ReactDOM = requireReactDom(); - function formatProdErrorMessage(code2) { - var url = "https://react.dev/errors/" + code2; - if (1 < arguments.length) { - url += "?args[]=" + encodeURIComponent(arguments[1]); - for (var i4 = 2; i4 < arguments.length; i4++) - url += "&args[]=" + encodeURIComponent(arguments[i4]); - } - return "Minified React error #" + code2 + "; visit " + url + " for the full message or use the non-minified dev environment for full errors and additional helpful warnings."; - } - function isValidContainer(node2) { - return !(!node2 || 1 !== node2.nodeType && 9 !== node2.nodeType && 11 !== node2.nodeType); - } - function getNearestMountedFiber(fiber) { - var node2 = fiber, nearestMounted = fiber; - if (fiber.alternate) for (; node2.return; ) node2 = node2.return; - else { - fiber = node2; - do - node2 = fiber, 0 !== (node2.flags & 4098) && (nearestMounted = node2.return), fiber = node2.return; - while (fiber); - } - return 3 === node2.tag ? nearestMounted : null; - } - function getSuspenseInstanceFromFiber(fiber) { - if (13 === fiber.tag) { - var suspenseState = fiber.memoizedState; - null === suspenseState && (fiber = fiber.alternate, null !== fiber && (suspenseState = fiber.memoizedState)); - if (null !== suspenseState) return suspenseState.dehydrated; - } - return null; - } - function getActivityInstanceFromFiber(fiber) { - if (31 === fiber.tag) { - var activityState = fiber.memoizedState; - null === activityState && (fiber = fiber.alternate, null !== fiber && (activityState = fiber.memoizedState)); - if (null !== activityState) return activityState.dehydrated; - } - return null; - } - function assertIsMounted(fiber) { - if (getNearestMountedFiber(fiber) !== fiber) - throw Error(formatProdErrorMessage(188)); - } - function findCurrentFiberUsingSlowPath(fiber) { - var alternate = fiber.alternate; - if (!alternate) { - alternate = getNearestMountedFiber(fiber); - if (null === alternate) throw Error(formatProdErrorMessage(188)); - return alternate !== fiber ? null : fiber; - } - for (var a2 = fiber, b2 = alternate; ; ) { - var parentA = a2.return; - if (null === parentA) break; - var parentB = parentA.alternate; - if (null === parentB) { - b2 = parentA.return; - if (null !== b2) { - a2 = b2; - continue; - } - break; - } - if (parentA.child === parentB.child) { - for (parentB = parentA.child; parentB; ) { - if (parentB === a2) return assertIsMounted(parentA), fiber; - if (parentB === b2) return assertIsMounted(parentA), alternate; - parentB = parentB.sibling; - } - throw Error(formatProdErrorMessage(188)); - } - if (a2.return !== b2.return) a2 = parentA, b2 = parentB; - else { - for (var didFindChild = false, child$0 = parentA.child; child$0; ) { - if (child$0 === a2) { - didFindChild = true; - a2 = parentA; - b2 = parentB; - break; - } - if (child$0 === b2) { - didFindChild = true; - b2 = parentA; - a2 = parentB; - break; - } - child$0 = child$0.sibling; - } - if (!didFindChild) { - for (child$0 = parentB.child; child$0; ) { - if (child$0 === a2) { - didFindChild = true; - a2 = parentB; - b2 = parentA; - break; - } - if (child$0 === b2) { - didFindChild = true; - b2 = parentB; - a2 = parentA; - break; - } - child$0 = child$0.sibling; - } - if (!didFindChild) throw Error(formatProdErrorMessage(189)); - } - } - if (a2.alternate !== b2) throw Error(formatProdErrorMessage(190)); - } - if (3 !== a2.tag) throw Error(formatProdErrorMessage(188)); - return a2.stateNode.current === a2 ? fiber : alternate; - } - function findCurrentHostFiberImpl(node2) { - var tag = node2.tag; - if (5 === tag || 26 === tag || 27 === tag || 6 === tag) return node2; - for (node2 = node2.child; null !== node2; ) { - tag = findCurrentHostFiberImpl(node2); - if (null !== tag) return tag; - node2 = node2.sibling; - } - return null; - } - var assign2 = Object.assign, REACT_LEGACY_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.element"), REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = /* @__PURE__ */ Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = /* @__PURE__ */ Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = /* @__PURE__ */ Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = /* @__PURE__ */ Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = /* @__PURE__ */ Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = /* @__PURE__ */ Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = /* @__PURE__ */ Symbol.for("react.memo"), REACT_LAZY_TYPE = /* @__PURE__ */ Symbol.for("react.lazy"); - var REACT_ACTIVITY_TYPE = /* @__PURE__ */ Symbol.for("react.activity"); - var REACT_MEMO_CACHE_SENTINEL = /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel"); - var MAYBE_ITERATOR_SYMBOL = Symbol.iterator; - function getIteratorFn(maybeIterable) { - if (null === maybeIterable || "object" !== typeof maybeIterable) return null; - maybeIterable = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable["@@iterator"]; - return "function" === typeof maybeIterable ? maybeIterable : null; - } - var REACT_CLIENT_REFERENCE = /* @__PURE__ */ Symbol.for("react.client.reference"); - function getComponentNameFromType(type) { - if (null == type) return null; - if ("function" === typeof type) - return type.$$typeof === REACT_CLIENT_REFERENCE ? null : type.displayName || type.name || null; - if ("string" === typeof type) return type; - switch (type) { - case REACT_FRAGMENT_TYPE: - return "Fragment"; - case REACT_PROFILER_TYPE: - return "Profiler"; - case REACT_STRICT_MODE_TYPE: - return "StrictMode"; - case REACT_SUSPENSE_TYPE: - return "Suspense"; - case REACT_SUSPENSE_LIST_TYPE: - return "SuspenseList"; - case REACT_ACTIVITY_TYPE: - return "Activity"; - } - if ("object" === typeof type) - switch (type.$$typeof) { - case REACT_PORTAL_TYPE: - return "Portal"; - case REACT_CONTEXT_TYPE: - return type.displayName || "Context"; - case REACT_CONSUMER_TYPE: - return (type._context.displayName || "Context") + ".Consumer"; - case REACT_FORWARD_REF_TYPE: - var innerType = type.render; - type = type.displayName; - type || (type = innerType.displayName || innerType.name || "", type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef"); - return type; - case REACT_MEMO_TYPE: - return innerType = type.displayName || null, null !== innerType ? innerType : getComponentNameFromType(type.type) || "Memo"; - case REACT_LAZY_TYPE: - innerType = type._payload; - type = type._init; - try { - return getComponentNameFromType(type(innerType)); - } catch (x2) { - } - } - return null; - } - var isArrayImpl = Array.isArray, ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, ReactDOMSharedInternals = ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, sharedNotPendingObject = { - pending: false, - data: null, - method: null, - action: null - }, valueStack = [], index = -1; - function createCursor(defaultValue) { - return { current: defaultValue }; - } - function pop2(cursor) { - 0 > index || (cursor.current = valueStack[index], valueStack[index] = null, index--); - } - function push2(cursor, value2) { - index++; - valueStack[index] = cursor.current; - cursor.current = value2; - } - var contextStackCursor = createCursor(null), contextFiberStackCursor = createCursor(null), rootInstanceStackCursor = createCursor(null), hostTransitionProviderCursor = createCursor(null); - function pushHostContainer(fiber, nextRootInstance) { - push2(rootInstanceStackCursor, nextRootInstance); - push2(contextFiberStackCursor, fiber); - push2(contextStackCursor, null); - switch (nextRootInstance.nodeType) { - case 9: - case 11: - fiber = (fiber = nextRootInstance.documentElement) ? (fiber = fiber.namespaceURI) ? getOwnHostContext(fiber) : 0 : 0; - break; - default: - if (fiber = nextRootInstance.tagName, nextRootInstance = nextRootInstance.namespaceURI) - nextRootInstance = getOwnHostContext(nextRootInstance), fiber = getChildHostContextProd(nextRootInstance, fiber); - else - switch (fiber) { - case "svg": - fiber = 1; - break; - case "math": - fiber = 2; - break; - default: - fiber = 0; - } - } - pop2(contextStackCursor); - push2(contextStackCursor, fiber); - } - function popHostContainer() { - pop2(contextStackCursor); - pop2(contextFiberStackCursor); - pop2(rootInstanceStackCursor); - } - function pushHostContext(fiber) { - null !== fiber.memoizedState && push2(hostTransitionProviderCursor, fiber); - var context = contextStackCursor.current; - var JSCompiler_inline_result = getChildHostContextProd(context, fiber.type); - context !== JSCompiler_inline_result && (push2(contextFiberStackCursor, fiber), push2(contextStackCursor, JSCompiler_inline_result)); - } - function popHostContext(fiber) { - contextFiberStackCursor.current === fiber && (pop2(contextStackCursor), pop2(contextFiberStackCursor)); - hostTransitionProviderCursor.current === fiber && (pop2(hostTransitionProviderCursor), HostTransitionContext._currentValue = sharedNotPendingObject); - } - var prefix, suffix; - function describeBuiltInComponentFrame(name2) { - if (void 0 === prefix) - try { - throw Error(); - } catch (x2) { - var match3 = x2.stack.trim().match(/\n( *(at )?)/); - prefix = match3 && match3[1] || ""; - suffix = -1 < x2.stack.indexOf("\n at") ? " ()" : -1 < x2.stack.indexOf("@") ? "@unknown:0:0" : ""; - } - return "\n" + prefix + name2 + suffix; - } - var reentry = false; - function describeNativeComponentFrame(fn3, construct) { - if (!fn3 || reentry) return ""; - reentry = true; - var previousPrepareStackTrace = Error.prepareStackTrace; - Error.prepareStackTrace = void 0; - try { - var RunInRootFrame = { - DetermineComponentFrameRoot: function() { - try { - if (construct) { - var Fake = function() { - throw Error(); - }; - Object.defineProperty(Fake.prototype, "props", { - set: function() { - throw Error(); - } - }); - if ("object" === typeof Reflect && Reflect.construct) { - try { - Reflect.construct(Fake, []); - } catch (x2) { - var control = x2; - } - Reflect.construct(fn3, [], Fake); - } else { - try { - Fake.call(); - } catch (x$12) { - control = x$12; - } - fn3.call(Fake.prototype); - } - } else { - try { - throw Error(); - } catch (x$2) { - control = x$2; - } - (Fake = fn3()) && "function" === typeof Fake.catch && Fake.catch(function() { - }); - } - } catch (sample2) { - if (sample2 && control && "string" === typeof sample2.stack) - return [sample2.stack, control.stack]; - } - return [null, null]; - } - }; - RunInRootFrame.DetermineComponentFrameRoot.displayName = "DetermineComponentFrameRoot"; - var namePropDescriptor = Object.getOwnPropertyDescriptor( - RunInRootFrame.DetermineComponentFrameRoot, - "name" - ); - namePropDescriptor && namePropDescriptor.configurable && Object.defineProperty( - RunInRootFrame.DetermineComponentFrameRoot, - "name", - { value: "DetermineComponentFrameRoot" } - ); - var _RunInRootFrame$Deter = RunInRootFrame.DetermineComponentFrameRoot(), sampleStack = _RunInRootFrame$Deter[0], controlStack = _RunInRootFrame$Deter[1]; - if (sampleStack && controlStack) { - var sampleLines = sampleStack.split("\n"), controlLines = controlStack.split("\n"); - for (namePropDescriptor = RunInRootFrame = 0; RunInRootFrame < sampleLines.length && !sampleLines[RunInRootFrame].includes("DetermineComponentFrameRoot"); ) - RunInRootFrame++; - for (; namePropDescriptor < controlLines.length && !controlLines[namePropDescriptor].includes( - "DetermineComponentFrameRoot" - ); ) - namePropDescriptor++; - if (RunInRootFrame === sampleLines.length || namePropDescriptor === controlLines.length) - for (RunInRootFrame = sampleLines.length - 1, namePropDescriptor = controlLines.length - 1; 1 <= RunInRootFrame && 0 <= namePropDescriptor && sampleLines[RunInRootFrame] !== controlLines[namePropDescriptor]; ) - namePropDescriptor--; - for (; 1 <= RunInRootFrame && 0 <= namePropDescriptor; RunInRootFrame--, namePropDescriptor--) - if (sampleLines[RunInRootFrame] !== controlLines[namePropDescriptor]) { - if (1 !== RunInRootFrame || 1 !== namePropDescriptor) { - do - if (RunInRootFrame--, namePropDescriptor--, 0 > namePropDescriptor || sampleLines[RunInRootFrame] !== controlLines[namePropDescriptor]) { - var frame = "\n" + sampleLines[RunInRootFrame].replace(" at new ", " at "); - fn3.displayName && frame.includes("") && (frame = frame.replace("", fn3.displayName)); - return frame; - } - while (1 <= RunInRootFrame && 0 <= namePropDescriptor); - } - break; - } - } - } finally { - reentry = false, Error.prepareStackTrace = previousPrepareStackTrace; - } - return (previousPrepareStackTrace = fn3 ? fn3.displayName || fn3.name : "") ? describeBuiltInComponentFrame(previousPrepareStackTrace) : ""; - } - function describeFiber(fiber, childFiber) { - switch (fiber.tag) { - case 26: - case 27: - case 5: - return describeBuiltInComponentFrame(fiber.type); - case 16: - return describeBuiltInComponentFrame("Lazy"); - case 13: - return fiber.child !== childFiber && null !== childFiber ? describeBuiltInComponentFrame("Suspense Fallback") : describeBuiltInComponentFrame("Suspense"); - case 19: - return describeBuiltInComponentFrame("SuspenseList"); - case 0: - case 15: - return describeNativeComponentFrame(fiber.type, false); - case 11: - return describeNativeComponentFrame(fiber.type.render, false); - case 1: - return describeNativeComponentFrame(fiber.type, true); - case 31: - return describeBuiltInComponentFrame("Activity"); - default: - return ""; - } - } - function getStackByFiberInDevAndProd(workInProgress2) { - try { - var info = "", previous = null; - do - info += describeFiber(workInProgress2, previous), previous = workInProgress2, workInProgress2 = workInProgress2.return; - while (workInProgress2); - return info; - } catch (x2) { - return "\nError generating stack: " + x2.message + "\n" + x2.stack; - } - } - var hasOwnProperty2 = Object.prototype.hasOwnProperty, scheduleCallback$3 = Scheduler.unstable_scheduleCallback, cancelCallback$1 = Scheduler.unstable_cancelCallback, shouldYield = Scheduler.unstable_shouldYield, requestPaint = Scheduler.unstable_requestPaint, now2 = Scheduler.unstable_now, getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel, ImmediatePriority = Scheduler.unstable_ImmediatePriority, UserBlockingPriority = Scheduler.unstable_UserBlockingPriority, NormalPriority$1 = Scheduler.unstable_NormalPriority, LowPriority = Scheduler.unstable_LowPriority, IdlePriority = Scheduler.unstable_IdlePriority, log$12 = Scheduler.log, unstable_setDisableYieldValue = Scheduler.unstable_setDisableYieldValue, rendererID = null, injectedHook = null; - function setIsStrictModeForDevtools(newIsStrictMode) { - "function" === typeof log$12 && unstable_setDisableYieldValue(newIsStrictMode); - if (injectedHook && "function" === typeof injectedHook.setStrictMode) - try { - injectedHook.setStrictMode(rendererID, newIsStrictMode); - } catch (err2) { - } - } - var clz322 = Math.clz32 ? Math.clz32 : clz32Fallback, log3 = Math.log, LN2 = Math.LN2; - function clz32Fallback(x2) { - x2 >>>= 0; - return 0 === x2 ? 32 : 31 - (log3(x2) / LN2 | 0) | 0; - } - var nextTransitionUpdateLane = 256, nextTransitionDeferredLane = 262144, nextRetryLane = 4194304; - function getHighestPriorityLanes(lanes) { - var pendingSyncLanes = lanes & 42; - if (0 !== pendingSyncLanes) return pendingSyncLanes; - switch (lanes & -lanes) { - 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 lanes & 261888; - case 262144: - case 524288: - case 1048576: - case 2097152: - return lanes & 3932160; - case 4194304: - case 8388608: - case 16777216: - case 33554432: - return lanes & 62914560; - case 67108864: - return 67108864; - case 134217728: - return 134217728; - case 268435456: - return 268435456; - case 536870912: - return 536870912; - case 1073741824: - return 0; - default: - return lanes; - } - } - function getNextLanes(root3, wipLanes, rootHasPendingCommit) { - var pendingLanes = root3.pendingLanes; - if (0 === pendingLanes) return 0; - var nextLanes = 0, suspendedLanes = root3.suspendedLanes, pingedLanes = root3.pingedLanes; - root3 = root3.warmLanes; - var nonIdlePendingLanes = pendingLanes & 134217727; - 0 !== nonIdlePendingLanes ? (pendingLanes = nonIdlePendingLanes & ~suspendedLanes, 0 !== pendingLanes ? nextLanes = getHighestPriorityLanes(pendingLanes) : (pingedLanes &= nonIdlePendingLanes, 0 !== pingedLanes ? nextLanes = getHighestPriorityLanes(pingedLanes) : rootHasPendingCommit || (rootHasPendingCommit = nonIdlePendingLanes & ~root3, 0 !== rootHasPendingCommit && (nextLanes = getHighestPriorityLanes(rootHasPendingCommit))))) : (nonIdlePendingLanes = pendingLanes & ~suspendedLanes, 0 !== nonIdlePendingLanes ? nextLanes = getHighestPriorityLanes(nonIdlePendingLanes) : 0 !== pingedLanes ? nextLanes = getHighestPriorityLanes(pingedLanes) : rootHasPendingCommit || (rootHasPendingCommit = pendingLanes & ~root3, 0 !== rootHasPendingCommit && (nextLanes = getHighestPriorityLanes(rootHasPendingCommit)))); - return 0 === nextLanes ? 0 : 0 !== wipLanes && wipLanes !== nextLanes && 0 === (wipLanes & suspendedLanes) && (suspendedLanes = nextLanes & -nextLanes, rootHasPendingCommit = wipLanes & -wipLanes, suspendedLanes >= rootHasPendingCommit || 32 === suspendedLanes && 0 !== (rootHasPendingCommit & 4194048)) ? wipLanes : nextLanes; - } - function checkIfRootIsPrerendering(root3, renderLanes2) { - return 0 === (root3.pendingLanes & ~(root3.suspendedLanes & ~root3.pingedLanes) & renderLanes2); - } - function computeExpirationTime(lane, currentTime) { - switch (lane) { - case 1: - case 2: - case 4: - case 8: - case 64: - return currentTime + 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 currentTime + 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 claimNextRetryLane() { - var lane = nextRetryLane; - nextRetryLane <<= 1; - 0 === (nextRetryLane & 62914560) && (nextRetryLane = 4194304); - return lane; - } - function createLaneMap(initial) { - for (var laneMap = [], i4 = 0; 31 > i4; i4++) laneMap.push(initial); - return laneMap; - } - function markRootUpdated$1(root3, updateLane) { - root3.pendingLanes |= updateLane; - 268435456 !== updateLane && (root3.suspendedLanes = 0, root3.pingedLanes = 0, root3.warmLanes = 0); - } - function markRootFinished(root3, finishedLanes, remainingLanes, spawnedLane, updatedLanes, suspendedRetryLanes) { - var previouslyPendingLanes = root3.pendingLanes; - root3.pendingLanes = remainingLanes; - root3.suspendedLanes = 0; - root3.pingedLanes = 0; - root3.warmLanes = 0; - root3.expiredLanes &= remainingLanes; - root3.entangledLanes &= remainingLanes; - root3.errorRecoveryDisabledLanes &= remainingLanes; - root3.shellSuspendCounter = 0; - var entanglements = root3.entanglements, expirationTimes = root3.expirationTimes, hiddenUpdates = root3.hiddenUpdates; - for (remainingLanes = previouslyPendingLanes & ~remainingLanes; 0 < remainingLanes; ) { - var index$7 = 31 - clz322(remainingLanes), lane = 1 << index$7; - entanglements[index$7] = 0; - expirationTimes[index$7] = -1; - var hiddenUpdatesForLane = hiddenUpdates[index$7]; - if (null !== hiddenUpdatesForLane) - for (hiddenUpdates[index$7] = null, index$7 = 0; index$7 < hiddenUpdatesForLane.length; index$7++) { - var update2 = hiddenUpdatesForLane[index$7]; - null !== update2 && (update2.lane &= -536870913); - } - remainingLanes &= ~lane; - } - 0 !== spawnedLane && markSpawnedDeferredLane(root3, spawnedLane, 0); - 0 !== suspendedRetryLanes && 0 === updatedLanes && 0 !== root3.tag && (root3.suspendedLanes |= suspendedRetryLanes & ~(previouslyPendingLanes & ~finishedLanes)); - } - function markSpawnedDeferredLane(root3, spawnedLane, entangledLanes) { - root3.pendingLanes |= spawnedLane; - root3.suspendedLanes &= ~spawnedLane; - var spawnedLaneIndex = 31 - clz322(spawnedLane); - root3.entangledLanes |= spawnedLane; - root3.entanglements[spawnedLaneIndex] = root3.entanglements[spawnedLaneIndex] | 1073741824 | entangledLanes & 261930; - } - function markRootEntangled(root3, entangledLanes) { - var rootEntangledLanes = root3.entangledLanes |= entangledLanes; - for (root3 = root3.entanglements; rootEntangledLanes; ) { - var index$8 = 31 - clz322(rootEntangledLanes), lane = 1 << index$8; - lane & entangledLanes | root3[index$8] & entangledLanes && (root3[index$8] |= entangledLanes); - rootEntangledLanes &= ~lane; - } - } - function getBumpedLaneForHydration(root3, renderLanes2) { - var renderLane = renderLanes2 & -renderLanes2; - renderLane = 0 !== (renderLane & 42) ? 1 : getBumpedLaneForHydrationByLane(renderLane); - return 0 !== (renderLane & (root3.suspendedLanes | renderLanes2)) ? 0 : renderLane; - } - function getBumpedLaneForHydrationByLane(lane) { - switch (lane) { - case 2: - lane = 1; - break; - case 8: - lane = 4; - break; - case 32: - lane = 16; - break; - 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: - case 4194304: - case 8388608: - case 16777216: - case 33554432: - lane = 128; - break; - case 268435456: - lane = 134217728; - break; - default: - lane = 0; - } - return lane; - } - function lanesToEventPriority(lanes) { - lanes &= -lanes; - return 2 < lanes ? 8 < lanes ? 0 !== (lanes & 134217727) ? 32 : 268435456 : 8 : 2; - } - function resolveUpdatePriority() { - var updatePriority = ReactDOMSharedInternals.p; - if (0 !== updatePriority) return updatePriority; - updatePriority = window.event; - return void 0 === updatePriority ? 32 : getEventPriority(updatePriority.type); - } - function runWithPriority(priority, fn3) { - var previousPriority = ReactDOMSharedInternals.p; - try { - return ReactDOMSharedInternals.p = priority, fn3(); - } finally { - ReactDOMSharedInternals.p = previousPriority; - } - } - var randomKey = Math.random().toString(36).slice(2), internalInstanceKey = "__reactFiber$" + randomKey, internalPropsKey = "__reactProps$" + randomKey, internalContainerInstanceKey = "__reactContainer$" + randomKey, internalEventHandlersKey = "__reactEvents$" + randomKey, internalEventHandlerListenersKey = "__reactListeners$" + randomKey, internalEventHandlesSetKey = "__reactHandles$" + randomKey, internalRootNodeResourcesKey = "__reactResources$" + randomKey, internalHoistableMarker = "__reactMarker$" + randomKey; - function detachDeletedInstance(node2) { - delete node2[internalInstanceKey]; - delete node2[internalPropsKey]; - delete node2[internalEventHandlersKey]; - delete node2[internalEventHandlerListenersKey]; - delete node2[internalEventHandlesSetKey]; - } - function getClosestInstanceFromNode(targetNode) { - var targetInst = targetNode[internalInstanceKey]; - if (targetInst) return targetInst; - for (var parentNode = targetNode.parentNode; parentNode; ) { - if (targetInst = parentNode[internalContainerInstanceKey] || parentNode[internalInstanceKey]) { - parentNode = targetInst.alternate; - if (null !== targetInst.child || null !== parentNode && null !== parentNode.child) - for (targetNode = getParentHydrationBoundary(targetNode); null !== targetNode; ) { - if (parentNode = targetNode[internalInstanceKey]) return parentNode; - targetNode = getParentHydrationBoundary(targetNode); - } - return targetInst; - } - targetNode = parentNode; - parentNode = targetNode.parentNode; - } - return null; - } - function getInstanceFromNode(node2) { - if (node2 = node2[internalInstanceKey] || node2[internalContainerInstanceKey]) { - var tag = node2.tag; - if (5 === tag || 6 === tag || 13 === tag || 31 === tag || 26 === tag || 27 === tag || 3 === tag) - return node2; - } - return null; - } - function getNodeFromInstance(inst) { - var tag = inst.tag; - if (5 === tag || 26 === tag || 27 === tag || 6 === tag) return inst.stateNode; - throw Error(formatProdErrorMessage(33)); - } - function getResourcesFromRoot(root3) { - var resources = root3[internalRootNodeResourcesKey]; - resources || (resources = root3[internalRootNodeResourcesKey] = { hoistableStyles: /* @__PURE__ */ new Map(), hoistableScripts: /* @__PURE__ */ new Map() }); - return resources; - } - function markNodeAsHoistable(node2) { - node2[internalHoistableMarker] = true; - } - var allNativeEvents = /* @__PURE__ */ new Set(), registrationNameDependencies = {}; - function registerTwoPhaseEvent(registrationName, dependencies) { - registerDirectEvent(registrationName, dependencies); - registerDirectEvent(registrationName + "Capture", dependencies); - } - function registerDirectEvent(registrationName, dependencies) { - registrationNameDependencies[registrationName] = dependencies; - for (registrationName = 0; registrationName < dependencies.length; registrationName++) - allNativeEvents.add(dependencies[registrationName]); - } - var VALID_ATTRIBUTE_NAME_REGEX = RegExp( - "^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*$" - ), illegalAttributeNameCache = {}, validatedAttributeNameCache = {}; - function isAttributeNameSafe(attributeName) { - if (hasOwnProperty2.call(validatedAttributeNameCache, attributeName)) - return true; - if (hasOwnProperty2.call(illegalAttributeNameCache, attributeName)) return false; - if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) - return validatedAttributeNameCache[attributeName] = true; - illegalAttributeNameCache[attributeName] = true; - return false; - } - function setValueForAttribute(node2, name2, value2) { - if (isAttributeNameSafe(name2)) - if (null === value2) node2.removeAttribute(name2); - else { - switch (typeof value2) { - case "undefined": - case "function": - case "symbol": - node2.removeAttribute(name2); - return; - case "boolean": - var prefix$10 = name2.toLowerCase().slice(0, 5); - if ("data-" !== prefix$10 && "aria-" !== prefix$10) { - node2.removeAttribute(name2); - return; - } - } - node2.setAttribute(name2, "" + value2); - } - } - function setValueForKnownAttribute(node2, name2, value2) { - if (null === value2) node2.removeAttribute(name2); - else { - switch (typeof value2) { - case "undefined": - case "function": - case "symbol": - case "boolean": - node2.removeAttribute(name2); - return; - } - node2.setAttribute(name2, "" + value2); - } - } - function setValueForNamespacedAttribute(node2, namespace, name2, value2) { - if (null === value2) node2.removeAttribute(name2); - else { - switch (typeof value2) { - case "undefined": - case "function": - case "symbol": - case "boolean": - node2.removeAttribute(name2); - return; - } - node2.setAttributeNS(namespace, name2, "" + value2); - } - } - function getToStringValue(value2) { - switch (typeof value2) { - case "bigint": - case "boolean": - case "number": - case "string": - case "undefined": - return value2; - case "object": - return value2; - default: - return ""; - } - } - function isCheckable(elem) { - var type = elem.type; - return (elem = elem.nodeName) && "input" === elem.toLowerCase() && ("checkbox" === type || "radio" === type); - } - function trackValueOnNode(node2, valueField, currentValue) { - var descriptor = Object.getOwnPropertyDescriptor( - node2.constructor.prototype, - valueField - ); - if (!node2.hasOwnProperty(valueField) && "undefined" !== typeof descriptor && "function" === typeof descriptor.get && "function" === typeof descriptor.set) { - var get2 = descriptor.get, set3 = descriptor.set; - Object.defineProperty(node2, valueField, { - configurable: true, - get: function() { - return get2.call(this); - }, - set: function(value2) { - currentValue = "" + value2; - set3.call(this, value2); - } - }); - Object.defineProperty(node2, valueField, { - enumerable: descriptor.enumerable - }); - return { - getValue: function() { - return currentValue; - }, - setValue: function(value2) { - currentValue = "" + value2; - }, - stopTracking: function() { - node2._valueTracker = null; - delete node2[valueField]; - } - }; - } - } - function track(node2) { - if (!node2._valueTracker) { - var valueField = isCheckable(node2) ? "checked" : "value"; - node2._valueTracker = trackValueOnNode( - node2, - valueField, - "" + node2[valueField] - ); - } - } - function updateValueIfChanged(node2) { - if (!node2) return false; - var tracker = node2._valueTracker; - if (!tracker) return true; - var lastValue = tracker.getValue(); - var value2 = ""; - node2 && (value2 = isCheckable(node2) ? node2.checked ? "true" : "false" : node2.value); - node2 = value2; - return node2 !== lastValue ? (tracker.setValue(node2), true) : false; - } - function getActiveElement(doc) { - doc = doc || ("undefined" !== typeof document ? document : void 0); - if ("undefined" === typeof doc) return null; - try { - return doc.activeElement || doc.body; - } catch (e3) { - return doc.body; - } - } - var escapeSelectorAttributeValueInsideDoubleQuotesRegex = /[\n"\\]/g; - function escapeSelectorAttributeValueInsideDoubleQuotes(value2) { - return value2.replace( - escapeSelectorAttributeValueInsideDoubleQuotesRegex, - function(ch) { - return "\\" + ch.charCodeAt(0).toString(16) + " "; - } - ); - } - function updateInput(element, value2, defaultValue, lastDefaultValue, checked, defaultChecked, type, name2) { - element.name = ""; - null != type && "function" !== typeof type && "symbol" !== typeof type && "boolean" !== typeof type ? element.type = type : element.removeAttribute("type"); - if (null != value2) - if ("number" === type) { - if (0 === value2 && "" === element.value || element.value != value2) - element.value = "" + getToStringValue(value2); - } else - element.value !== "" + getToStringValue(value2) && (element.value = "" + getToStringValue(value2)); - else - "submit" !== type && "reset" !== type || element.removeAttribute("value"); - null != value2 ? setDefaultValue(element, type, getToStringValue(value2)) : null != defaultValue ? setDefaultValue(element, type, getToStringValue(defaultValue)) : null != lastDefaultValue && element.removeAttribute("value"); - null == checked && null != defaultChecked && (element.defaultChecked = !!defaultChecked); - null != checked && (element.checked = checked && "function" !== typeof checked && "symbol" !== typeof checked); - null != name2 && "function" !== typeof name2 && "symbol" !== typeof name2 && "boolean" !== typeof name2 ? element.name = "" + getToStringValue(name2) : element.removeAttribute("name"); - } - function initInput(element, value2, defaultValue, checked, defaultChecked, type, name2, isHydrating2) { - null != type && "function" !== typeof type && "symbol" !== typeof type && "boolean" !== typeof type && (element.type = type); - if (null != value2 || null != defaultValue) { - if (!("submit" !== type && "reset" !== type || void 0 !== value2 && null !== value2)) { - track(element); - return; - } - defaultValue = null != defaultValue ? "" + getToStringValue(defaultValue) : ""; - value2 = null != value2 ? "" + getToStringValue(value2) : defaultValue; - isHydrating2 || value2 === element.value || (element.value = value2); - element.defaultValue = value2; - } - checked = null != checked ? checked : defaultChecked; - checked = "function" !== typeof checked && "symbol" !== typeof checked && !!checked; - element.checked = isHydrating2 ? element.checked : !!checked; - element.defaultChecked = !!checked; - null != name2 && "function" !== typeof name2 && "symbol" !== typeof name2 && "boolean" !== typeof name2 && (element.name = name2); - track(element); - } - function setDefaultValue(node2, type, value2) { - "number" === type && getActiveElement(node2.ownerDocument) === node2 || node2.defaultValue === "" + value2 || (node2.defaultValue = "" + value2); - } - function updateOptions(node2, multiple, propValue, setDefaultSelected) { - node2 = node2.options; - if (multiple) { - multiple = {}; - for (var i4 = 0; i4 < propValue.length; i4++) - multiple["$" + propValue[i4]] = true; - for (propValue = 0; propValue < node2.length; propValue++) - i4 = multiple.hasOwnProperty("$" + node2[propValue].value), node2[propValue].selected !== i4 && (node2[propValue].selected = i4), i4 && setDefaultSelected && (node2[propValue].defaultSelected = true); - } else { - propValue = "" + getToStringValue(propValue); - multiple = null; - for (i4 = 0; i4 < node2.length; i4++) { - if (node2[i4].value === propValue) { - node2[i4].selected = true; - setDefaultSelected && (node2[i4].defaultSelected = true); - return; - } - null !== multiple || node2[i4].disabled || (multiple = node2[i4]); - } - null !== multiple && (multiple.selected = true); - } - } - function updateTextarea(element, value2, defaultValue) { - if (null != value2 && (value2 = "" + getToStringValue(value2), value2 !== element.value && (element.value = value2), null == defaultValue)) { - element.defaultValue !== value2 && (element.defaultValue = value2); - return; - } - element.defaultValue = null != defaultValue ? "" + getToStringValue(defaultValue) : ""; - } - function initTextarea(element, value2, defaultValue, children2) { - if (null == value2) { - if (null != children2) { - if (null != defaultValue) throw Error(formatProdErrorMessage(92)); - if (isArrayImpl(children2)) { - if (1 < children2.length) throw Error(formatProdErrorMessage(93)); - children2 = children2[0]; - } - defaultValue = children2; - } - null == defaultValue && (defaultValue = ""); - value2 = defaultValue; - } - defaultValue = getToStringValue(value2); - element.defaultValue = defaultValue; - children2 = element.textContent; - children2 === defaultValue && "" !== children2 && null !== children2 && (element.value = children2); - track(element); - } - function setTextContent(node2, text2) { - if (text2) { - var firstChild = node2.firstChild; - if (firstChild && firstChild === node2.lastChild && 3 === firstChild.nodeType) { - firstChild.nodeValue = text2; - return; - } - } - node2.textContent = text2; - } - var unitlessNumbers = new Set( - "animationIterationCount aspectRatio borderImageOutset borderImageSlice borderImageWidth boxFlex boxFlexGroup boxOrdinalGroup columnCount columns flex flexGrow flexPositive flexShrink flexNegative flexOrder gridArea gridRow gridRowEnd gridRowSpan gridRowStart gridColumn gridColumnEnd gridColumnSpan gridColumnStart fontWeight lineClamp lineHeight opacity order orphans scale tabSize widows zIndex zoom fillOpacity floodOpacity stopOpacity strokeDasharray strokeDashoffset strokeMiterlimit strokeOpacity strokeWidth MozAnimationIterationCount MozBoxFlex MozBoxFlexGroup MozLineClamp msAnimationIterationCount msFlex msZoom msFlexGrow msFlexNegative msFlexOrder msFlexPositive msFlexShrink msGridColumn msGridColumnSpan msGridRow msGridRowSpan WebkitAnimationIterationCount WebkitBoxFlex WebKitBoxFlexGroup WebkitBoxOrdinalGroup WebkitColumnCount WebkitColumns WebkitFlex WebkitFlexGrow WebkitFlexPositive WebkitFlexShrink WebkitLineClamp".split( - " " - ) - ); - function setValueForStyle(style3, styleName, value2) { - var isCustomProperty = 0 === styleName.indexOf("--"); - null == value2 || "boolean" === typeof value2 || "" === value2 ? isCustomProperty ? style3.setProperty(styleName, "") : "float" === styleName ? style3.cssFloat = "" : style3[styleName] = "" : isCustomProperty ? style3.setProperty(styleName, value2) : "number" !== typeof value2 || 0 === value2 || unitlessNumbers.has(styleName) ? "float" === styleName ? style3.cssFloat = value2 : style3[styleName] = ("" + value2).trim() : style3[styleName] = value2 + "px"; - } - function setValueForStyles(node2, styles2, prevStyles) { - if (null != styles2 && "object" !== typeof styles2) - throw Error(formatProdErrorMessage(62)); - node2 = node2.style; - if (null != prevStyles) { - for (var styleName in prevStyles) - !prevStyles.hasOwnProperty(styleName) || null != styles2 && styles2.hasOwnProperty(styleName) || (0 === styleName.indexOf("--") ? node2.setProperty(styleName, "") : "float" === styleName ? node2.cssFloat = "" : node2[styleName] = ""); - for (var styleName$16 in styles2) - styleName = styles2[styleName$16], styles2.hasOwnProperty(styleName$16) && prevStyles[styleName$16] !== styleName && setValueForStyle(node2, styleName$16, styleName); - } else - for (var styleName$17 in styles2) - styles2.hasOwnProperty(styleName$17) && setValueForStyle(node2, styleName$17, styles2[styleName$17]); - } - function isCustomElement(tagName) { - if (-1 === tagName.indexOf("-")) return false; - switch (tagName) { - case "annotation-xml": - case "color-profile": - case "font-face": - case "font-face-src": - case "font-face-uri": - case "font-face-format": - case "font-face-name": - case "missing-glyph": - return false; - default: - return true; - } - } - var aliases = /* @__PURE__ */ new Map([ - ["acceptCharset", "accept-charset"], - ["htmlFor", "for"], - ["httpEquiv", "http-equiv"], - ["crossOrigin", "crossorigin"], - ["accentHeight", "accent-height"], - ["alignmentBaseline", "alignment-baseline"], - ["arabicForm", "arabic-form"], - ["baselineShift", "baseline-shift"], - ["capHeight", "cap-height"], - ["clipPath", "clip-path"], - ["clipRule", "clip-rule"], - ["colorInterpolation", "color-interpolation"], - ["colorInterpolationFilters", "color-interpolation-filters"], - ["colorProfile", "color-profile"], - ["colorRendering", "color-rendering"], - ["dominantBaseline", "dominant-baseline"], - ["enableBackground", "enable-background"], - ["fillOpacity", "fill-opacity"], - ["fillRule", "fill-rule"], - ["floodColor", "flood-color"], - ["floodOpacity", "flood-opacity"], - ["fontFamily", "font-family"], - ["fontSize", "font-size"], - ["fontSizeAdjust", "font-size-adjust"], - ["fontStretch", "font-stretch"], - ["fontStyle", "font-style"], - ["fontVariant", "font-variant"], - ["fontWeight", "font-weight"], - ["glyphName", "glyph-name"], - ["glyphOrientationHorizontal", "glyph-orientation-horizontal"], - ["glyphOrientationVertical", "glyph-orientation-vertical"], - ["horizAdvX", "horiz-adv-x"], - ["horizOriginX", "horiz-origin-x"], - ["imageRendering", "image-rendering"], - ["letterSpacing", "letter-spacing"], - ["lightingColor", "lighting-color"], - ["markerEnd", "marker-end"], - ["markerMid", "marker-mid"], - ["markerStart", "marker-start"], - ["overlinePosition", "overline-position"], - ["overlineThickness", "overline-thickness"], - ["paintOrder", "paint-order"], - ["panose-1", "panose-1"], - ["pointerEvents", "pointer-events"], - ["renderingIntent", "rendering-intent"], - ["shapeRendering", "shape-rendering"], - ["stopColor", "stop-color"], - ["stopOpacity", "stop-opacity"], - ["strikethroughPosition", "strikethrough-position"], - ["strikethroughThickness", "strikethrough-thickness"], - ["strokeDasharray", "stroke-dasharray"], - ["strokeDashoffset", "stroke-dashoffset"], - ["strokeLinecap", "stroke-linecap"], - ["strokeLinejoin", "stroke-linejoin"], - ["strokeMiterlimit", "stroke-miterlimit"], - ["strokeOpacity", "stroke-opacity"], - ["strokeWidth", "stroke-width"], - ["textAnchor", "text-anchor"], - ["textDecoration", "text-decoration"], - ["textRendering", "text-rendering"], - ["transformOrigin", "transform-origin"], - ["underlinePosition", "underline-position"], - ["underlineThickness", "underline-thickness"], - ["unicodeBidi", "unicode-bidi"], - ["unicodeRange", "unicode-range"], - ["unitsPerEm", "units-per-em"], - ["vAlphabetic", "v-alphabetic"], - ["vHanging", "v-hanging"], - ["vIdeographic", "v-ideographic"], - ["vMathematical", "v-mathematical"], - ["vectorEffect", "vector-effect"], - ["vertAdvY", "vert-adv-y"], - ["vertOriginX", "vert-origin-x"], - ["vertOriginY", "vert-origin-y"], - ["wordSpacing", "word-spacing"], - ["writingMode", "writing-mode"], - ["xmlnsXlink", "xmlns:xlink"], - ["xHeight", "x-height"] - ]), isJavaScriptProtocol = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i; - function sanitizeURL(url) { - return isJavaScriptProtocol.test("" + url) ? "javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')" : url; - } - function noop$12() { - } - var currentReplayingEvent = null; - function getEventTarget(nativeEvent) { - nativeEvent = nativeEvent.target || nativeEvent.srcElement || window; - nativeEvent.correspondingUseElement && (nativeEvent = nativeEvent.correspondingUseElement); - return 3 === nativeEvent.nodeType ? nativeEvent.parentNode : nativeEvent; - } - var restoreTarget = null, restoreQueue = null; - function restoreStateOfTarget(target2) { - var internalInstance = getInstanceFromNode(target2); - if (internalInstance && (target2 = internalInstance.stateNode)) { - var props = target2[internalPropsKey] || null; - a: switch (target2 = internalInstance.stateNode, internalInstance.type) { - case "input": - updateInput( - target2, - props.value, - props.defaultValue, - props.defaultValue, - props.checked, - props.defaultChecked, - props.type, - props.name - ); - internalInstance = props.name; - if ("radio" === props.type && null != internalInstance) { - for (props = target2; props.parentNode; ) props = props.parentNode; - props = props.querySelectorAll( - 'input[name="' + escapeSelectorAttributeValueInsideDoubleQuotes( - "" + internalInstance - ) + '"][type="radio"]' - ); - for (internalInstance = 0; internalInstance < props.length; internalInstance++) { - var otherNode = props[internalInstance]; - if (otherNode !== target2 && otherNode.form === target2.form) { - var otherProps = otherNode[internalPropsKey] || null; - if (!otherProps) throw Error(formatProdErrorMessage(90)); - updateInput( - otherNode, - otherProps.value, - otherProps.defaultValue, - otherProps.defaultValue, - otherProps.checked, - otherProps.defaultChecked, - otherProps.type, - otherProps.name - ); - } - } - for (internalInstance = 0; internalInstance < props.length; internalInstance++) - otherNode = props[internalInstance], otherNode.form === target2.form && updateValueIfChanged(otherNode); - } - break a; - case "textarea": - updateTextarea(target2, props.value, props.defaultValue); - break a; - case "select": - internalInstance = props.value, null != internalInstance && updateOptions(target2, !!props.multiple, internalInstance, false); - } - } - } - var isInsideEventHandler = false; - function batchedUpdates$1(fn3, a2, b2) { - if (isInsideEventHandler) return fn3(a2, b2); - isInsideEventHandler = true; - try { - var JSCompiler_inline_result = fn3(a2); - return JSCompiler_inline_result; - } finally { - if (isInsideEventHandler = false, null !== restoreTarget || null !== restoreQueue) { - if (flushSyncWork$1(), restoreTarget && (a2 = restoreTarget, fn3 = restoreQueue, restoreQueue = restoreTarget = null, restoreStateOfTarget(a2), fn3)) - for (a2 = 0; a2 < fn3.length; a2++) restoreStateOfTarget(fn3[a2]); - } - } - } - function getListener2(inst, registrationName) { - var stateNode = inst.stateNode; - if (null === stateNode) return null; - var props = stateNode[internalPropsKey] || null; - if (null === props) return null; - stateNode = props[registrationName]; - a: switch (registrationName) { - case "onClick": - case "onClickCapture": - case "onDoubleClick": - case "onDoubleClickCapture": - case "onMouseDown": - case "onMouseDownCapture": - case "onMouseMove": - case "onMouseMoveCapture": - case "onMouseUp": - case "onMouseUpCapture": - case "onMouseEnter": - (props = !props.disabled) || (inst = inst.type, props = !("button" === inst || "input" === inst || "select" === inst || "textarea" === inst)); - inst = !props; - break a; - default: - inst = false; - } - if (inst) return null; - if (stateNode && "function" !== typeof stateNode) - throw Error( - formatProdErrorMessage(231, registrationName, typeof stateNode) - ); - return stateNode; - } - var canUseDOM = !("undefined" === typeof window || "undefined" === typeof window.document || "undefined" === typeof window.document.createElement), passiveBrowserEventsSupported = false; - if (canUseDOM) - try { - var options = {}; - Object.defineProperty(options, "passive", { - get: function() { - passiveBrowserEventsSupported = true; - } - }); - window.addEventListener("test", options, options); - window.removeEventListener("test", options, options); - } catch (e3) { - passiveBrowserEventsSupported = false; - } - var root2 = null, startText = null, fallbackText = null; - function getData() { - if (fallbackText) return fallbackText; - var start2, startValue = startText, startLength = startValue.length, end2, endValue = "value" in root2 ? root2.value : root2.textContent, endLength = endValue.length; - for (start2 = 0; start2 < startLength && startValue[start2] === endValue[start2]; start2++) ; - var minEnd = startLength - start2; - for (end2 = 1; end2 <= minEnd && startValue[startLength - end2] === endValue[endLength - end2]; end2++) ; - return fallbackText = endValue.slice(start2, 1 < end2 ? 1 - end2 : void 0); - } - function getEventCharCode(nativeEvent) { - var keyCode = nativeEvent.keyCode; - "charCode" in nativeEvent ? (nativeEvent = nativeEvent.charCode, 0 === nativeEvent && 13 === keyCode && (nativeEvent = 13)) : nativeEvent = keyCode; - 10 === nativeEvent && (nativeEvent = 13); - return 32 <= nativeEvent || 13 === nativeEvent ? nativeEvent : 0; - } - function functionThatReturnsTrue() { - return true; - } - function functionThatReturnsFalse() { - return false; - } - function createSyntheticEvent(Interface) { - function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) { - this._reactName = reactName; - this._targetInst = targetInst; - this.type = reactEventType; - this.nativeEvent = nativeEvent; - this.target = nativeEventTarget; - this.currentTarget = null; - for (var propName in Interface) - Interface.hasOwnProperty(propName) && (reactName = Interface[propName], this[propName] = reactName ? reactName(nativeEvent) : nativeEvent[propName]); - this.isDefaultPrevented = (null != nativeEvent.defaultPrevented ? nativeEvent.defaultPrevented : false === nativeEvent.returnValue) ? functionThatReturnsTrue : functionThatReturnsFalse; - this.isPropagationStopped = functionThatReturnsFalse; - return this; - } - assign2(SyntheticBaseEvent.prototype, { - preventDefault: function() { - this.defaultPrevented = true; - var event = this.nativeEvent; - event && (event.preventDefault ? event.preventDefault() : "unknown" !== typeof event.returnValue && (event.returnValue = false), this.isDefaultPrevented = functionThatReturnsTrue); - }, - stopPropagation: function() { - var event = this.nativeEvent; - event && (event.stopPropagation ? event.stopPropagation() : "unknown" !== typeof event.cancelBubble && (event.cancelBubble = true), this.isPropagationStopped = functionThatReturnsTrue); - }, - persist: function() { - }, - isPersistent: functionThatReturnsTrue - }); - return SyntheticBaseEvent; - } - var EventInterface = { - eventPhase: 0, - bubbles: 0, - cancelable: 0, - timeStamp: function(event) { - return event.timeStamp || Date.now(); - }, - defaultPrevented: 0, - isTrusted: 0 - }, SyntheticEvent = createSyntheticEvent(EventInterface), UIEventInterface = assign2({}, EventInterface, { view: 0, detail: 0 }), SyntheticUIEvent = createSyntheticEvent(UIEventInterface), lastMovementX, lastMovementY, lastMouseEvent, MouseEventInterface = assign2({}, UIEventInterface, { - screenX: 0, - screenY: 0, - clientX: 0, - clientY: 0, - pageX: 0, - pageY: 0, - ctrlKey: 0, - shiftKey: 0, - altKey: 0, - metaKey: 0, - getModifierState: getEventModifierState, - button: 0, - buttons: 0, - relatedTarget: function(event) { - return void 0 === event.relatedTarget ? event.fromElement === event.srcElement ? event.toElement : event.fromElement : event.relatedTarget; - }, - movementX: function(event) { - if ("movementX" in event) return event.movementX; - event !== lastMouseEvent && (lastMouseEvent && "mousemove" === event.type ? (lastMovementX = event.screenX - lastMouseEvent.screenX, lastMovementY = event.screenY - lastMouseEvent.screenY) : lastMovementY = lastMovementX = 0, lastMouseEvent = event); - return lastMovementX; - }, - movementY: function(event) { - return "movementY" in event ? event.movementY : lastMovementY; - } - }), SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface), DragEventInterface = assign2({}, MouseEventInterface, { dataTransfer: 0 }), SyntheticDragEvent = createSyntheticEvent(DragEventInterface), FocusEventInterface = assign2({}, UIEventInterface, { relatedTarget: 0 }), SyntheticFocusEvent = createSyntheticEvent(FocusEventInterface), AnimationEventInterface = assign2({}, EventInterface, { - animationName: 0, - elapsedTime: 0, - pseudoElement: 0 - }), SyntheticAnimationEvent = createSyntheticEvent(AnimationEventInterface), ClipboardEventInterface = assign2({}, EventInterface, { - clipboardData: function(event) { - return "clipboardData" in event ? event.clipboardData : window.clipboardData; - } - }), SyntheticClipboardEvent = createSyntheticEvent(ClipboardEventInterface), CompositionEventInterface = assign2({}, EventInterface, { data: 0 }), SyntheticCompositionEvent = createSyntheticEvent(CompositionEventInterface), normalizeKey = { - Esc: "Escape", - Spacebar: " ", - Left: "ArrowLeft", - Up: "ArrowUp", - Right: "ArrowRight", - Down: "ArrowDown", - Del: "Delete", - Win: "OS", - Menu: "ContextMenu", - Apps: "ContextMenu", - Scroll: "ScrollLock", - MozPrintableKey: "Unidentified" - }, translateToKey = { - 8: "Backspace", - 9: "Tab", - 12: "Clear", - 13: "Enter", - 16: "Shift", - 17: "Control", - 18: "Alt", - 19: "Pause", - 20: "CapsLock", - 27: "Escape", - 32: " ", - 33: "PageUp", - 34: "PageDown", - 35: "End", - 36: "Home", - 37: "ArrowLeft", - 38: "ArrowUp", - 39: "ArrowRight", - 40: "ArrowDown", - 45: "Insert", - 46: "Delete", - 112: "F1", - 113: "F2", - 114: "F3", - 115: "F4", - 116: "F5", - 117: "F6", - 118: "F7", - 119: "F8", - 120: "F9", - 121: "F10", - 122: "F11", - 123: "F12", - 144: "NumLock", - 145: "ScrollLock", - 224: "Meta" - }, modifierKeyToProp = { - Alt: "altKey", - Control: "ctrlKey", - Meta: "metaKey", - Shift: "shiftKey" - }; - function modifierStateGetter(keyArg) { - var nativeEvent = this.nativeEvent; - return nativeEvent.getModifierState ? nativeEvent.getModifierState(keyArg) : (keyArg = modifierKeyToProp[keyArg]) ? !!nativeEvent[keyArg] : false; - } - function getEventModifierState() { - return modifierStateGetter; - } - var KeyboardEventInterface = assign2({}, UIEventInterface, { - key: function(nativeEvent) { - if (nativeEvent.key) { - var key2 = normalizeKey[nativeEvent.key] || nativeEvent.key; - if ("Unidentified" !== key2) return key2; - } - return "keypress" === nativeEvent.type ? (nativeEvent = getEventCharCode(nativeEvent), 13 === nativeEvent ? "Enter" : String.fromCharCode(nativeEvent)) : "keydown" === nativeEvent.type || "keyup" === nativeEvent.type ? translateToKey[nativeEvent.keyCode] || "Unidentified" : ""; - }, - code: 0, - location: 0, - ctrlKey: 0, - shiftKey: 0, - altKey: 0, - metaKey: 0, - repeat: 0, - locale: 0, - getModifierState: getEventModifierState, - charCode: function(event) { - return "keypress" === event.type ? getEventCharCode(event) : 0; - }, - keyCode: function(event) { - return "keydown" === event.type || "keyup" === event.type ? event.keyCode : 0; - }, - which: function(event) { - return "keypress" === event.type ? getEventCharCode(event) : "keydown" === event.type || "keyup" === event.type ? event.keyCode : 0; - } - }), SyntheticKeyboardEvent = createSyntheticEvent(KeyboardEventInterface), PointerEventInterface = assign2({}, MouseEventInterface, { - pointerId: 0, - width: 0, - height: 0, - pressure: 0, - tangentialPressure: 0, - tiltX: 0, - tiltY: 0, - twist: 0, - pointerType: 0, - isPrimary: 0 - }), SyntheticPointerEvent = createSyntheticEvent(PointerEventInterface), TouchEventInterface = assign2({}, UIEventInterface, { - touches: 0, - targetTouches: 0, - changedTouches: 0, - altKey: 0, - metaKey: 0, - ctrlKey: 0, - shiftKey: 0, - getModifierState: getEventModifierState - }), SyntheticTouchEvent = createSyntheticEvent(TouchEventInterface), TransitionEventInterface = assign2({}, EventInterface, { - propertyName: 0, - elapsedTime: 0, - pseudoElement: 0 - }), SyntheticTransitionEvent = createSyntheticEvent(TransitionEventInterface), WheelEventInterface = assign2({}, MouseEventInterface, { - deltaX: function(event) { - return "deltaX" in event ? event.deltaX : "wheelDeltaX" in event ? -event.wheelDeltaX : 0; - }, - deltaY: function(event) { - return "deltaY" in event ? event.deltaY : "wheelDeltaY" in event ? -event.wheelDeltaY : "wheelDelta" in event ? -event.wheelDelta : 0; - }, - deltaZ: 0, - deltaMode: 0 - }), SyntheticWheelEvent = createSyntheticEvent(WheelEventInterface), ToggleEventInterface = assign2({}, EventInterface, { - newState: 0, - oldState: 0 - }), SyntheticToggleEvent = createSyntheticEvent(ToggleEventInterface), END_KEYCODES = [9, 13, 27, 32], canUseCompositionEvent = canUseDOM && "CompositionEvent" in window, documentMode = null; - canUseDOM && "documentMode" in document && (documentMode = document.documentMode); - var canUseTextInputEvent = canUseDOM && "TextEvent" in window && !documentMode, useFallbackCompositionData = canUseDOM && (!canUseCompositionEvent || documentMode && 8 < documentMode && 11 >= documentMode), SPACEBAR_CHAR = String.fromCharCode(32), hasSpaceKeypress = false; - function isFallbackCompositionEnd(domEventName, nativeEvent) { - switch (domEventName) { - case "keyup": - return -1 !== END_KEYCODES.indexOf(nativeEvent.keyCode); - case "keydown": - return 229 !== nativeEvent.keyCode; - case "keypress": - case "mousedown": - case "focusout": - return true; - default: - return false; - } - } - function getDataFromCustomEvent(nativeEvent) { - nativeEvent = nativeEvent.detail; - return "object" === typeof nativeEvent && "data" in nativeEvent ? nativeEvent.data : null; - } - var isComposing = false; - function getNativeBeforeInputChars(domEventName, nativeEvent) { - switch (domEventName) { - case "compositionend": - return getDataFromCustomEvent(nativeEvent); - case "keypress": - if (32 !== nativeEvent.which) return null; - hasSpaceKeypress = true; - return SPACEBAR_CHAR; - case "textInput": - return domEventName = nativeEvent.data, domEventName === SPACEBAR_CHAR && hasSpaceKeypress ? null : domEventName; - default: - return null; - } - } - function getFallbackBeforeInputChars(domEventName, nativeEvent) { - if (isComposing) - return "compositionend" === domEventName || !canUseCompositionEvent && isFallbackCompositionEnd(domEventName, nativeEvent) ? (domEventName = getData(), fallbackText = startText = root2 = null, isComposing = false, domEventName) : null; - switch (domEventName) { - case "paste": - return null; - case "keypress": - if (!(nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) || nativeEvent.ctrlKey && nativeEvent.altKey) { - if (nativeEvent.char && 1 < nativeEvent.char.length) - return nativeEvent.char; - if (nativeEvent.which) return String.fromCharCode(nativeEvent.which); - } - return null; - case "compositionend": - return useFallbackCompositionData && "ko" !== nativeEvent.locale ? null : nativeEvent.data; - default: - return null; - } - } - var supportedInputTypes = { - color: true, - date: true, - datetime: true, - "datetime-local": true, - email: true, - month: true, - number: true, - password: true, - range: true, - search: true, - tel: true, - text: true, - time: true, - url: true, - week: true - }; - function isTextInputElement(elem) { - var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase(); - return "input" === nodeName ? !!supportedInputTypes[elem.type] : "textarea" === nodeName ? true : false; - } - function createAndAccumulateChangeEvent(dispatchQueue, inst, nativeEvent, target2) { - restoreTarget ? restoreQueue ? restoreQueue.push(target2) : restoreQueue = [target2] : restoreTarget = target2; - inst = accumulateTwoPhaseListeners(inst, "onChange"); - 0 < inst.length && (nativeEvent = new SyntheticEvent( - "onChange", - "change", - null, - nativeEvent, - target2 - ), dispatchQueue.push({ event: nativeEvent, listeners: inst })); - } - var activeElement$1 = null, activeElementInst$1 = null; - function runEventInBatch(dispatchQueue) { - processDispatchQueue(dispatchQueue, 0); - } - function getInstIfValueChanged(targetInst) { - var targetNode = getNodeFromInstance(targetInst); - if (updateValueIfChanged(targetNode)) return targetInst; - } - function getTargetInstForChangeEvent(domEventName, targetInst) { - if ("change" === domEventName) return targetInst; - } - var isInputEventSupported = false; - if (canUseDOM) { - var JSCompiler_inline_result$jscomp$286; - if (canUseDOM) { - var isSupported$jscomp$inline_427 = "oninput" in document; - if (!isSupported$jscomp$inline_427) { - var element$jscomp$inline_428 = document.createElement("div"); - element$jscomp$inline_428.setAttribute("oninput", "return;"); - isSupported$jscomp$inline_427 = "function" === typeof element$jscomp$inline_428.oninput; - } - JSCompiler_inline_result$jscomp$286 = isSupported$jscomp$inline_427; - } else JSCompiler_inline_result$jscomp$286 = false; - isInputEventSupported = JSCompiler_inline_result$jscomp$286 && (!document.documentMode || 9 < document.documentMode); - } - function stopWatchingForValueChange() { - activeElement$1 && (activeElement$1.detachEvent("onpropertychange", handlePropertyChange), activeElementInst$1 = activeElement$1 = null); - } - function handlePropertyChange(nativeEvent) { - if ("value" === nativeEvent.propertyName && getInstIfValueChanged(activeElementInst$1)) { - var dispatchQueue = []; - createAndAccumulateChangeEvent( - dispatchQueue, - activeElementInst$1, - nativeEvent, - getEventTarget(nativeEvent) - ); - batchedUpdates$1(runEventInBatch, dispatchQueue); - } - } - function handleEventsForInputEventPolyfill(domEventName, target2, targetInst) { - "focusin" === domEventName ? (stopWatchingForValueChange(), activeElement$1 = target2, activeElementInst$1 = targetInst, activeElement$1.attachEvent("onpropertychange", handlePropertyChange)) : "focusout" === domEventName && stopWatchingForValueChange(); - } - function getTargetInstForInputEventPolyfill(domEventName) { - if ("selectionchange" === domEventName || "keyup" === domEventName || "keydown" === domEventName) - return getInstIfValueChanged(activeElementInst$1); - } - function getTargetInstForClickEvent(domEventName, targetInst) { - if ("click" === domEventName) return getInstIfValueChanged(targetInst); - } - function getTargetInstForInputOrChangeEvent(domEventName, targetInst) { - if ("input" === domEventName || "change" === domEventName) - return getInstIfValueChanged(targetInst); - } - function is2(x2, y3) { - return x2 === y3 && (0 !== x2 || 1 / x2 === 1 / y3) || x2 !== x2 && y3 !== y3; - } - var objectIs = "function" === typeof Object.is ? Object.is : is2; - function shallowEqual(objA, objB) { - if (objectIs(objA, objB)) return true; - if ("object" !== typeof objA || null === objA || "object" !== typeof objB || null === objB) - return false; - var keysA = Object.keys(objA), keysB = Object.keys(objB); - if (keysA.length !== keysB.length) return false; - for (keysB = 0; keysB < keysA.length; keysB++) { - var currentKey = keysA[keysB]; - if (!hasOwnProperty2.call(objB, currentKey) || !objectIs(objA[currentKey], objB[currentKey])) - return false; - } - return true; - } - function getLeafNode(node2) { - for (; node2 && node2.firstChild; ) node2 = node2.firstChild; - return node2; - } - function getNodeForCharacterOffset(root3, offset3) { - var node2 = getLeafNode(root3); - root3 = 0; - for (var nodeEnd; node2; ) { - if (3 === node2.nodeType) { - nodeEnd = root3 + node2.textContent.length; - if (root3 <= offset3 && nodeEnd >= offset3) - return { node: node2, offset: offset3 - root3 }; - root3 = nodeEnd; - } - a: { - for (; node2; ) { - if (node2.nextSibling) { - node2 = node2.nextSibling; - break a; - } - node2 = node2.parentNode; - } - node2 = void 0; - } - node2 = getLeafNode(node2); - } - } - function containsNode(outerNode, innerNode) { - return outerNode && innerNode ? outerNode === innerNode ? true : outerNode && 3 === outerNode.nodeType ? false : innerNode && 3 === innerNode.nodeType ? containsNode(outerNode, innerNode.parentNode) : "contains" in outerNode ? outerNode.contains(innerNode) : outerNode.compareDocumentPosition ? !!(outerNode.compareDocumentPosition(innerNode) & 16) : false : false; - } - function getActiveElementDeep(containerInfo) { - containerInfo = null != containerInfo && null != containerInfo.ownerDocument && null != containerInfo.ownerDocument.defaultView ? containerInfo.ownerDocument.defaultView : window; - for (var element = getActiveElement(containerInfo.document); element instanceof containerInfo.HTMLIFrameElement; ) { - try { - var JSCompiler_inline_result = "string" === typeof element.contentWindow.location.href; - } catch (err2) { - JSCompiler_inline_result = false; - } - if (JSCompiler_inline_result) containerInfo = element.contentWindow; - else break; - element = getActiveElement(containerInfo.document); - } - return element; - } - function hasSelectionCapabilities(elem) { - var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase(); - return nodeName && ("input" === nodeName && ("text" === elem.type || "search" === elem.type || "tel" === elem.type || "url" === elem.type || "password" === elem.type) || "textarea" === nodeName || "true" === elem.contentEditable); - } - var skipSelectionChangeEvent = canUseDOM && "documentMode" in document && 11 >= document.documentMode, activeElement = null, activeElementInst = null, lastSelection = null, mouseDown = false; - function constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget) { - var doc = nativeEventTarget.window === nativeEventTarget ? nativeEventTarget.document : 9 === nativeEventTarget.nodeType ? nativeEventTarget : nativeEventTarget.ownerDocument; - mouseDown || null == activeElement || activeElement !== getActiveElement(doc) || (doc = activeElement, "selectionStart" in doc && hasSelectionCapabilities(doc) ? doc = { start: doc.selectionStart, end: doc.selectionEnd } : (doc = (doc.ownerDocument && doc.ownerDocument.defaultView || window).getSelection(), doc = { - anchorNode: doc.anchorNode, - anchorOffset: doc.anchorOffset, - focusNode: doc.focusNode, - focusOffset: doc.focusOffset - }), lastSelection && shallowEqual(lastSelection, doc) || (lastSelection = doc, doc = accumulateTwoPhaseListeners(activeElementInst, "onSelect"), 0 < doc.length && (nativeEvent = new SyntheticEvent( - "onSelect", - "select", - null, - nativeEvent, - nativeEventTarget - ), dispatchQueue.push({ event: nativeEvent, listeners: doc }), nativeEvent.target = activeElement))); - } - function makePrefixMap(styleProp, eventName) { - var prefixes = {}; - prefixes[styleProp.toLowerCase()] = eventName.toLowerCase(); - prefixes["Webkit" + styleProp] = "webkit" + eventName; - prefixes["Moz" + styleProp] = "moz" + eventName; - return prefixes; - } - var vendorPrefixes = { - animationend: makePrefixMap("Animation", "AnimationEnd"), - animationiteration: makePrefixMap("Animation", "AnimationIteration"), - animationstart: makePrefixMap("Animation", "AnimationStart"), - transitionrun: makePrefixMap("Transition", "TransitionRun"), - transitionstart: makePrefixMap("Transition", "TransitionStart"), - transitioncancel: makePrefixMap("Transition", "TransitionCancel"), - transitionend: makePrefixMap("Transition", "TransitionEnd") - }, prefixedEventNames = {}, style2 = {}; - canUseDOM && (style2 = document.createElement("div").style, "AnimationEvent" in window || (delete vendorPrefixes.animationend.animation, delete vendorPrefixes.animationiteration.animation, delete vendorPrefixes.animationstart.animation), "TransitionEvent" in window || delete vendorPrefixes.transitionend.transition); - function getVendorPrefixedEventName(eventName) { - if (prefixedEventNames[eventName]) return prefixedEventNames[eventName]; - if (!vendorPrefixes[eventName]) return eventName; - var prefixMap = vendorPrefixes[eventName], styleProp; - for (styleProp in prefixMap) - if (prefixMap.hasOwnProperty(styleProp) && styleProp in style2) - return prefixedEventNames[eventName] = prefixMap[styleProp]; - return eventName; - } - var ANIMATION_END = getVendorPrefixedEventName("animationend"), ANIMATION_ITERATION = getVendorPrefixedEventName("animationiteration"), ANIMATION_START = getVendorPrefixedEventName("animationstart"), TRANSITION_RUN = getVendorPrefixedEventName("transitionrun"), TRANSITION_START = getVendorPrefixedEventName("transitionstart"), TRANSITION_CANCEL = getVendorPrefixedEventName("transitioncancel"), TRANSITION_END = getVendorPrefixedEventName("transitionend"), topLevelEventsToReactNames = /* @__PURE__ */ new Map(), simpleEventPluginEvents = "abort auxClick beforeToggle cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split( - " " - ); - simpleEventPluginEvents.push("scrollEnd"); - function registerSimpleEvent(domEventName, reactName) { - topLevelEventsToReactNames.set(domEventName, reactName); - registerTwoPhaseEvent(reactName, [domEventName]); - } - var reportGlobalError = "function" === typeof reportError ? reportError : function(error2) { - if ("object" === typeof window && "function" === typeof window.ErrorEvent) { - var event = new window.ErrorEvent("error", { - bubbles: true, - cancelable: true, - message: "object" === typeof error2 && null !== error2 && "string" === typeof error2.message ? String(error2.message) : String(error2), - error: error2 - }); - if (!window.dispatchEvent(event)) return; - } else if ("object" === typeof process && "function" === typeof process.emit) { - process.emit("uncaughtException", error2); - return; - } - console.error(error2); - }, concurrentQueues = [], concurrentQueuesIndex = 0, concurrentlyUpdatedLanes = 0; - function finishQueueingConcurrentUpdates() { - for (var endIndex = concurrentQueuesIndex, i4 = concurrentlyUpdatedLanes = concurrentQueuesIndex = 0; i4 < endIndex; ) { - var fiber = concurrentQueues[i4]; - concurrentQueues[i4++] = null; - var queue = concurrentQueues[i4]; - concurrentQueues[i4++] = null; - var update2 = concurrentQueues[i4]; - concurrentQueues[i4++] = null; - var lane = concurrentQueues[i4]; - concurrentQueues[i4++] = null; - if (null !== queue && null !== update2) { - var pending = queue.pending; - null === pending ? update2.next = update2 : (update2.next = pending.next, pending.next = update2); - queue.pending = update2; - } - 0 !== lane && markUpdateLaneFromFiberToRoot(fiber, update2, lane); - } - } - function enqueueUpdate$1(fiber, queue, update2, lane) { - concurrentQueues[concurrentQueuesIndex++] = fiber; - concurrentQueues[concurrentQueuesIndex++] = queue; - concurrentQueues[concurrentQueuesIndex++] = update2; - concurrentQueues[concurrentQueuesIndex++] = lane; - concurrentlyUpdatedLanes |= lane; - fiber.lanes |= lane; - fiber = fiber.alternate; - null !== fiber && (fiber.lanes |= lane); - } - function enqueueConcurrentHookUpdate(fiber, queue, update2, lane) { - enqueueUpdate$1(fiber, queue, update2, lane); - return getRootForUpdatedFiber(fiber); - } - function enqueueConcurrentRenderForLane(fiber, lane) { - enqueueUpdate$1(fiber, null, null, lane); - return getRootForUpdatedFiber(fiber); - } - function markUpdateLaneFromFiberToRoot(sourceFiber, update2, lane) { - sourceFiber.lanes |= lane; - var alternate = sourceFiber.alternate; - null !== alternate && (alternate.lanes |= lane); - for (var isHidden = false, parent = sourceFiber.return; null !== parent; ) - parent.childLanes |= lane, alternate = parent.alternate, null !== alternate && (alternate.childLanes |= lane), 22 === parent.tag && (sourceFiber = parent.stateNode, null === sourceFiber || sourceFiber._visibility & 1 || (isHidden = true)), sourceFiber = parent, parent = parent.return; - return 3 === sourceFiber.tag ? (parent = sourceFiber.stateNode, isHidden && null !== update2 && (isHidden = 31 - clz322(lane), sourceFiber = parent.hiddenUpdates, alternate = sourceFiber[isHidden], null === alternate ? sourceFiber[isHidden] = [update2] : alternate.push(update2), update2.lane = lane | 536870912), parent) : null; - } - function getRootForUpdatedFiber(sourceFiber) { - if (50 < nestedUpdateCount) - throw nestedUpdateCount = 0, rootWithNestedUpdates = null, Error(formatProdErrorMessage(185)); - for (var parent = sourceFiber.return; null !== parent; ) - sourceFiber = parent, parent = sourceFiber.return; - return 3 === sourceFiber.tag ? sourceFiber.stateNode : null; - } - var emptyContextObject = {}; - function FiberNode(tag, pendingProps, key2, mode) { - this.tag = tag; - this.key = key2; - this.sibling = this.child = this.return = this.stateNode = this.type = this.elementType = null; - this.index = 0; - this.refCleanup = this.ref = null; - this.pendingProps = pendingProps; - this.dependencies = this.memoizedState = this.updateQueue = this.memoizedProps = null; - this.mode = mode; - this.subtreeFlags = this.flags = 0; - this.deletions = null; - this.childLanes = this.lanes = 0; - this.alternate = null; - } - function createFiberImplClass(tag, pendingProps, key2, mode) { - return new FiberNode(tag, pendingProps, key2, mode); - } - function shouldConstruct(Component2) { - Component2 = Component2.prototype; - return !(!Component2 || !Component2.isReactComponent); - } - function createWorkInProgress(current3, pendingProps) { - var workInProgress2 = current3.alternate; - null === workInProgress2 ? (workInProgress2 = createFiberImplClass( - current3.tag, - pendingProps, - current3.key, - current3.mode - ), workInProgress2.elementType = current3.elementType, workInProgress2.type = current3.type, workInProgress2.stateNode = current3.stateNode, workInProgress2.alternate = current3, current3.alternate = workInProgress2) : (workInProgress2.pendingProps = pendingProps, workInProgress2.type = current3.type, workInProgress2.flags = 0, workInProgress2.subtreeFlags = 0, workInProgress2.deletions = null); - workInProgress2.flags = current3.flags & 65011712; - workInProgress2.childLanes = current3.childLanes; - workInProgress2.lanes = current3.lanes; - workInProgress2.child = current3.child; - workInProgress2.memoizedProps = current3.memoizedProps; - workInProgress2.memoizedState = current3.memoizedState; - workInProgress2.updateQueue = current3.updateQueue; - pendingProps = current3.dependencies; - workInProgress2.dependencies = null === pendingProps ? null : { lanes: pendingProps.lanes, firstContext: pendingProps.firstContext }; - workInProgress2.sibling = current3.sibling; - workInProgress2.index = current3.index; - workInProgress2.ref = current3.ref; - workInProgress2.refCleanup = current3.refCleanup; - return workInProgress2; - } - function resetWorkInProgress(workInProgress2, renderLanes2) { - workInProgress2.flags &= 65011714; - var current3 = workInProgress2.alternate; - null === current3 ? (workInProgress2.childLanes = 0, workInProgress2.lanes = renderLanes2, workInProgress2.child = null, workInProgress2.subtreeFlags = 0, workInProgress2.memoizedProps = null, workInProgress2.memoizedState = null, workInProgress2.updateQueue = null, workInProgress2.dependencies = null, workInProgress2.stateNode = null) : (workInProgress2.childLanes = current3.childLanes, workInProgress2.lanes = current3.lanes, workInProgress2.child = current3.child, workInProgress2.subtreeFlags = 0, workInProgress2.deletions = null, workInProgress2.memoizedProps = current3.memoizedProps, workInProgress2.memoizedState = current3.memoizedState, workInProgress2.updateQueue = current3.updateQueue, workInProgress2.type = current3.type, renderLanes2 = current3.dependencies, workInProgress2.dependencies = null === renderLanes2 ? null : { - lanes: renderLanes2.lanes, - firstContext: renderLanes2.firstContext - }); - return workInProgress2; - } - function createFiberFromTypeAndProps(type, key2, pendingProps, owner, mode, lanes) { - var fiberTag = 0; - owner = type; - if ("function" === typeof type) shouldConstruct(type) && (fiberTag = 1); - else if ("string" === typeof type) - fiberTag = isHostHoistableType( - type, - pendingProps, - contextStackCursor.current - ) ? 26 : "html" === type || "head" === type || "body" === type ? 27 : 5; - else - a: switch (type) { - case REACT_ACTIVITY_TYPE: - return type = createFiberImplClass(31, pendingProps, key2, mode), type.elementType = REACT_ACTIVITY_TYPE, type.lanes = lanes, type; - case REACT_FRAGMENT_TYPE: - return createFiberFromFragment(pendingProps.children, mode, lanes, key2); - case REACT_STRICT_MODE_TYPE: - fiberTag = 8; - mode |= 24; - break; - case REACT_PROFILER_TYPE: - return type = createFiberImplClass(12, pendingProps, key2, mode | 2), type.elementType = REACT_PROFILER_TYPE, type.lanes = lanes, type; - case REACT_SUSPENSE_TYPE: - return type = createFiberImplClass(13, pendingProps, key2, mode), type.elementType = REACT_SUSPENSE_TYPE, type.lanes = lanes, type; - case REACT_SUSPENSE_LIST_TYPE: - return type = createFiberImplClass(19, pendingProps, key2, mode), type.elementType = REACT_SUSPENSE_LIST_TYPE, type.lanes = lanes, type; - default: - if ("object" === typeof type && null !== type) - switch (type.$$typeof) { - case REACT_CONTEXT_TYPE: - fiberTag = 10; - break a; - case REACT_CONSUMER_TYPE: - fiberTag = 9; - break a; - case REACT_FORWARD_REF_TYPE: - fiberTag = 11; - break a; - case REACT_MEMO_TYPE: - fiberTag = 14; - break a; - case REACT_LAZY_TYPE: - fiberTag = 16; - owner = null; - break a; - } - fiberTag = 29; - pendingProps = Error( - formatProdErrorMessage(130, null === type ? "null" : typeof type, "") - ); - owner = null; - } - key2 = createFiberImplClass(fiberTag, pendingProps, key2, mode); - key2.elementType = type; - key2.type = owner; - key2.lanes = lanes; - return key2; - } - function createFiberFromFragment(elements, mode, lanes, key2) { - elements = createFiberImplClass(7, elements, key2, mode); - elements.lanes = lanes; - return elements; - } - function createFiberFromText(content2, mode, lanes) { - content2 = createFiberImplClass(6, content2, null, mode); - content2.lanes = lanes; - return content2; - } - function createFiberFromDehydratedFragment(dehydratedNode) { - var fiber = createFiberImplClass(18, null, null, 0); - fiber.stateNode = dehydratedNode; - return fiber; - } - function createFiberFromPortal(portal, mode, lanes) { - mode = createFiberImplClass( - 4, - null !== portal.children ? portal.children : [], - portal.key, - mode - ); - mode.lanes = lanes; - mode.stateNode = { - containerInfo: portal.containerInfo, - pendingChildren: null, - implementation: portal.implementation - }; - return mode; - } - var CapturedStacks = /* @__PURE__ */ new WeakMap(); - function createCapturedValueAtFiber(value2, source2) { - if ("object" === typeof value2 && null !== value2) { - var existing = CapturedStacks.get(value2); - if (void 0 !== existing) return existing; - source2 = { - value: value2, - source: source2, - stack: getStackByFiberInDevAndProd(source2) - }; - CapturedStacks.set(value2, source2); - return source2; - } - return { - value: value2, - source: source2, - stack: getStackByFiberInDevAndProd(source2) - }; - } - var forkStack = [], forkStackIndex = 0, treeForkProvider = null, treeForkCount = 0, idStack = [], idStackIndex = 0, treeContextProvider = null, treeContextId = 1, treeContextOverflow = ""; - function pushTreeFork(workInProgress2, totalChildren) { - forkStack[forkStackIndex++] = treeForkCount; - forkStack[forkStackIndex++] = treeForkProvider; - treeForkProvider = workInProgress2; - treeForkCount = totalChildren; - } - function pushTreeId(workInProgress2, totalChildren, index2) { - idStack[idStackIndex++] = treeContextId; - idStack[idStackIndex++] = treeContextOverflow; - idStack[idStackIndex++] = treeContextProvider; - treeContextProvider = workInProgress2; - var baseIdWithLeadingBit = treeContextId; - workInProgress2 = treeContextOverflow; - var baseLength = 32 - clz322(baseIdWithLeadingBit) - 1; - baseIdWithLeadingBit &= ~(1 << baseLength); - index2 += 1; - var length2 = 32 - clz322(totalChildren) + baseLength; - if (30 < length2) { - var numberOfOverflowBits = baseLength - baseLength % 5; - length2 = (baseIdWithLeadingBit & (1 << numberOfOverflowBits) - 1).toString(32); - baseIdWithLeadingBit >>= numberOfOverflowBits; - baseLength -= numberOfOverflowBits; - treeContextId = 1 << 32 - clz322(totalChildren) + baseLength | index2 << baseLength | baseIdWithLeadingBit; - treeContextOverflow = length2 + workInProgress2; - } else - treeContextId = 1 << length2 | index2 << baseLength | baseIdWithLeadingBit, treeContextOverflow = workInProgress2; - } - function pushMaterializedTreeId(workInProgress2) { - null !== workInProgress2.return && (pushTreeFork(workInProgress2, 1), pushTreeId(workInProgress2, 1, 0)); - } - function popTreeContext(workInProgress2) { - for (; workInProgress2 === treeForkProvider; ) - treeForkProvider = forkStack[--forkStackIndex], forkStack[forkStackIndex] = null, treeForkCount = forkStack[--forkStackIndex], forkStack[forkStackIndex] = null; - for (; workInProgress2 === treeContextProvider; ) - treeContextProvider = idStack[--idStackIndex], idStack[idStackIndex] = null, treeContextOverflow = idStack[--idStackIndex], idStack[idStackIndex] = null, treeContextId = idStack[--idStackIndex], idStack[idStackIndex] = null; - } - function restoreSuspendedTreeContext(workInProgress2, suspendedContext) { - idStack[idStackIndex++] = treeContextId; - idStack[idStackIndex++] = treeContextOverflow; - idStack[idStackIndex++] = treeContextProvider; - treeContextId = suspendedContext.id; - treeContextOverflow = suspendedContext.overflow; - treeContextProvider = workInProgress2; - } - var hydrationParentFiber = null, nextHydratableInstance = null, isHydrating = false, hydrationErrors = null, rootOrSingletonContext = false, HydrationMismatchException = Error(formatProdErrorMessage(519)); - function throwOnHydrationMismatch(fiber) { - var error2 = Error( - formatProdErrorMessage( - 418, - 1 < arguments.length && void 0 !== arguments[1] && arguments[1] ? "text" : "HTML", - "" - ) - ); - queueHydrationError(createCapturedValueAtFiber(error2, fiber)); - throw HydrationMismatchException; - } - function prepareToHydrateHostInstance(fiber) { - var instance2 = fiber.stateNode, type = fiber.type, props = fiber.memoizedProps; - instance2[internalInstanceKey] = fiber; - instance2[internalPropsKey] = props; - switch (type) { - case "dialog": - listenToNonDelegatedEvent("cancel", instance2); - listenToNonDelegatedEvent("close", instance2); - break; - case "iframe": - case "object": - case "embed": - listenToNonDelegatedEvent("load", instance2); - break; - case "video": - case "audio": - for (type = 0; type < mediaEventTypes.length; type++) - listenToNonDelegatedEvent(mediaEventTypes[type], instance2); - break; - case "source": - listenToNonDelegatedEvent("error", instance2); - break; - case "img": - case "image": - case "link": - listenToNonDelegatedEvent("error", instance2); - listenToNonDelegatedEvent("load", instance2); - break; - case "details": - listenToNonDelegatedEvent("toggle", instance2); - break; - case "input": - listenToNonDelegatedEvent("invalid", instance2); - initInput( - instance2, - props.value, - props.defaultValue, - props.checked, - props.defaultChecked, - props.type, - props.name, - true - ); - break; - case "select": - listenToNonDelegatedEvent("invalid", instance2); - break; - case "textarea": - listenToNonDelegatedEvent("invalid", instance2), initTextarea(instance2, props.value, props.defaultValue, props.children); - } - type = props.children; - "string" !== typeof type && "number" !== typeof type && "bigint" !== typeof type || instance2.textContent === "" + type || true === props.suppressHydrationWarning || checkForUnmatchedText(instance2.textContent, type) ? (null != props.popover && (listenToNonDelegatedEvent("beforetoggle", instance2), listenToNonDelegatedEvent("toggle", instance2)), null != props.onScroll && listenToNonDelegatedEvent("scroll", instance2), null != props.onScrollEnd && listenToNonDelegatedEvent("scrollend", instance2), null != props.onClick && (instance2.onclick = noop$12), instance2 = true) : instance2 = false; - instance2 || throwOnHydrationMismatch(fiber, true); - } - function popToNextHostParent(fiber) { - for (hydrationParentFiber = fiber.return; hydrationParentFiber; ) - switch (hydrationParentFiber.tag) { - case 5: - case 31: - case 13: - rootOrSingletonContext = false; - return; - case 27: - case 3: - rootOrSingletonContext = true; - return; - default: - hydrationParentFiber = hydrationParentFiber.return; - } - } - function popHydrationState(fiber) { - if (fiber !== hydrationParentFiber) return false; - if (!isHydrating) return popToNextHostParent(fiber), isHydrating = true, false; - var tag = fiber.tag, JSCompiler_temp; - if (JSCompiler_temp = 3 !== tag && 27 !== tag) { - if (JSCompiler_temp = 5 === tag) - JSCompiler_temp = fiber.type, JSCompiler_temp = !("form" !== JSCompiler_temp && "button" !== JSCompiler_temp) || shouldSetTextContent(fiber.type, fiber.memoizedProps); - JSCompiler_temp = !JSCompiler_temp; - } - JSCompiler_temp && nextHydratableInstance && throwOnHydrationMismatch(fiber); - popToNextHostParent(fiber); - if (13 === tag) { - fiber = fiber.memoizedState; - fiber = null !== fiber ? fiber.dehydrated : null; - if (!fiber) throw Error(formatProdErrorMessage(317)); - nextHydratableInstance = getNextHydratableInstanceAfterHydrationBoundary(fiber); - } else if (31 === tag) { - fiber = fiber.memoizedState; - fiber = null !== fiber ? fiber.dehydrated : null; - if (!fiber) throw Error(formatProdErrorMessage(317)); - nextHydratableInstance = getNextHydratableInstanceAfterHydrationBoundary(fiber); - } else - 27 === tag ? (tag = nextHydratableInstance, isSingletonScope(fiber.type) ? (fiber = previousHydratableOnEnteringScopedSingleton, previousHydratableOnEnteringScopedSingleton = null, nextHydratableInstance = fiber) : nextHydratableInstance = tag) : nextHydratableInstance = hydrationParentFiber ? getNextHydratable(fiber.stateNode.nextSibling) : null; - return true; - } - function resetHydrationState() { - nextHydratableInstance = hydrationParentFiber = null; - isHydrating = false; - } - function upgradeHydrationErrorsToRecoverable() { - var queuedErrors = hydrationErrors; - null !== queuedErrors && (null === workInProgressRootRecoverableErrors ? workInProgressRootRecoverableErrors = queuedErrors : workInProgressRootRecoverableErrors.push.apply( - workInProgressRootRecoverableErrors, - queuedErrors - ), hydrationErrors = null); - return queuedErrors; - } - function queueHydrationError(error2) { - null === hydrationErrors ? hydrationErrors = [error2] : hydrationErrors.push(error2); - } - var valueCursor = createCursor(null), currentlyRenderingFiber$1 = null, lastContextDependency = null; - function pushProvider(providerFiber, context, nextValue) { - push2(valueCursor, context._currentValue); - context._currentValue = nextValue; - } - function popProvider(context) { - context._currentValue = valueCursor.current; - pop2(valueCursor); - } - function scheduleContextWorkOnParentPath(parent, renderLanes2, propagationRoot) { - for (; null !== parent; ) { - var alternate = parent.alternate; - (parent.childLanes & renderLanes2) !== renderLanes2 ? (parent.childLanes |= renderLanes2, null !== alternate && (alternate.childLanes |= renderLanes2)) : null !== alternate && (alternate.childLanes & renderLanes2) !== renderLanes2 && (alternate.childLanes |= renderLanes2); - if (parent === propagationRoot) break; - parent = parent.return; - } - } - function propagateContextChanges(workInProgress2, contexts, renderLanes2, forcePropagateEntireTree) { - var fiber = workInProgress2.child; - null !== fiber && (fiber.return = workInProgress2); - for (; null !== fiber; ) { - var list2 = fiber.dependencies; - if (null !== list2) { - var nextFiber = fiber.child; - list2 = list2.firstContext; - a: for (; null !== list2; ) { - var dependency = list2; - list2 = fiber; - for (var i4 = 0; i4 < contexts.length; i4++) - if (dependency.context === contexts[i4]) { - list2.lanes |= renderLanes2; - dependency = list2.alternate; - null !== dependency && (dependency.lanes |= renderLanes2); - scheduleContextWorkOnParentPath( - list2.return, - renderLanes2, - workInProgress2 - ); - forcePropagateEntireTree || (nextFiber = null); - break a; - } - list2 = dependency.next; - } - } else if (18 === fiber.tag) { - nextFiber = fiber.return; - if (null === nextFiber) throw Error(formatProdErrorMessage(341)); - nextFiber.lanes |= renderLanes2; - list2 = nextFiber.alternate; - null !== list2 && (list2.lanes |= renderLanes2); - scheduleContextWorkOnParentPath(nextFiber, renderLanes2, workInProgress2); - nextFiber = null; - } else nextFiber = fiber.child; - if (null !== nextFiber) nextFiber.return = fiber; - else - for (nextFiber = fiber; null !== nextFiber; ) { - if (nextFiber === workInProgress2) { - nextFiber = null; - break; - } - fiber = nextFiber.sibling; - if (null !== fiber) { - fiber.return = nextFiber.return; - nextFiber = fiber; - break; - } - nextFiber = nextFiber.return; - } - fiber = nextFiber; - } - } - function propagateParentContextChanges(current3, workInProgress2, renderLanes2, forcePropagateEntireTree) { - current3 = null; - for (var parent = workInProgress2, isInsidePropagationBailout = false; null !== parent; ) { - if (!isInsidePropagationBailout) { - if (0 !== (parent.flags & 524288)) isInsidePropagationBailout = true; - else if (0 !== (parent.flags & 262144)) break; - } - if (10 === parent.tag) { - var currentParent = parent.alternate; - if (null === currentParent) throw Error(formatProdErrorMessage(387)); - currentParent = currentParent.memoizedProps; - if (null !== currentParent) { - var context = parent.type; - objectIs(parent.pendingProps.value, currentParent.value) || (null !== current3 ? current3.push(context) : current3 = [context]); - } - } else if (parent === hostTransitionProviderCursor.current) { - currentParent = parent.alternate; - if (null === currentParent) throw Error(formatProdErrorMessage(387)); - currentParent.memoizedState.memoizedState !== parent.memoizedState.memoizedState && (null !== current3 ? current3.push(HostTransitionContext) : current3 = [HostTransitionContext]); - } - parent = parent.return; - } - null !== current3 && propagateContextChanges( - workInProgress2, - current3, - renderLanes2, - forcePropagateEntireTree - ); - workInProgress2.flags |= 262144; - } - function checkIfContextChanged(currentDependencies) { - for (currentDependencies = currentDependencies.firstContext; null !== currentDependencies; ) { - if (!objectIs( - currentDependencies.context._currentValue, - currentDependencies.memoizedValue - )) - return true; - currentDependencies = currentDependencies.next; - } - return false; - } - function prepareToReadContext(workInProgress2) { - currentlyRenderingFiber$1 = workInProgress2; - lastContextDependency = null; - workInProgress2 = workInProgress2.dependencies; - null !== workInProgress2 && (workInProgress2.firstContext = null); - } - function readContext(context) { - return readContextForConsumer(currentlyRenderingFiber$1, context); - } - function readContextDuringReconciliation(consumer, context) { - null === currentlyRenderingFiber$1 && prepareToReadContext(consumer); - return readContextForConsumer(consumer, context); - } - function readContextForConsumer(consumer, context) { - var value2 = context._currentValue; - context = { context, memoizedValue: value2, next: null }; - if (null === lastContextDependency) { - if (null === consumer) throw Error(formatProdErrorMessage(308)); - lastContextDependency = context; - consumer.dependencies = { lanes: 0, firstContext: context }; - consumer.flags |= 524288; - } else lastContextDependency = lastContextDependency.next = context; - return value2; - } - var AbortControllerLocal = "undefined" !== typeof AbortController ? AbortController : function() { - var listeners = [], signal = this.signal = { - aborted: false, - addEventListener: function(type, listener) { - listeners.push(listener); - } - }; - this.abort = function() { - signal.aborted = true; - listeners.forEach(function(listener) { - return listener(); - }); - }; - }, scheduleCallback$2 = Scheduler.unstable_scheduleCallback, NormalPriority = Scheduler.unstable_NormalPriority, CacheContext = { - $$typeof: REACT_CONTEXT_TYPE, - Consumer: null, - Provider: null, - _currentValue: null, - _currentValue2: null, - _threadCount: 0 - }; - function createCache() { - return { - controller: new AbortControllerLocal(), - data: /* @__PURE__ */ new Map(), - refCount: 0 - }; - } - function releaseCache(cache) { - cache.refCount--; - 0 === cache.refCount && scheduleCallback$2(NormalPriority, function() { - cache.controller.abort(); - }); - } - var currentEntangledListeners = null, currentEntangledPendingCount = 0, currentEntangledLane = 0, currentEntangledActionThenable = null; - function entangleAsyncAction(transition, thenable) { - if (null === currentEntangledListeners) { - var entangledListeners = currentEntangledListeners = []; - currentEntangledPendingCount = 0; - currentEntangledLane = requestTransitionLane(); - currentEntangledActionThenable = { - status: "pending", - value: void 0, - then: function(resolve2) { - entangledListeners.push(resolve2); - } - }; - } - currentEntangledPendingCount++; - thenable.then(pingEngtangledActionScope, pingEngtangledActionScope); - return thenable; - } - function pingEngtangledActionScope() { - if (0 === --currentEntangledPendingCount && null !== currentEntangledListeners) { - null !== currentEntangledActionThenable && (currentEntangledActionThenable.status = "fulfilled"); - var listeners = currentEntangledListeners; - currentEntangledListeners = null; - currentEntangledLane = 0; - currentEntangledActionThenable = null; - for (var i4 = 0; i4 < listeners.length; i4++) (0, listeners[i4])(); - } - } - function chainThenableValue(thenable, result2) { - var listeners = [], thenableWithOverride = { - status: "pending", - value: null, - reason: null, - then: function(resolve2) { - listeners.push(resolve2); - } - }; - thenable.then( - function() { - thenableWithOverride.status = "fulfilled"; - thenableWithOverride.value = result2; - for (var i4 = 0; i4 < listeners.length; i4++) (0, listeners[i4])(result2); - }, - function(error2) { - thenableWithOverride.status = "rejected"; - thenableWithOverride.reason = error2; - for (error2 = 0; error2 < listeners.length; error2++) - (0, listeners[error2])(void 0); - } - ); - return thenableWithOverride; - } - var prevOnStartTransitionFinish = ReactSharedInternals.S; - ReactSharedInternals.S = function(transition, returnValue) { - globalMostRecentTransitionTime = now2(); - "object" === typeof returnValue && null !== returnValue && "function" === typeof returnValue.then && entangleAsyncAction(transition, returnValue); - null !== prevOnStartTransitionFinish && prevOnStartTransitionFinish(transition, returnValue); - }; - var resumedCache = createCursor(null); - function peekCacheFromPool() { - var cacheResumedFromPreviousRender = resumedCache.current; - return null !== cacheResumedFromPreviousRender ? cacheResumedFromPreviousRender : workInProgressRoot.pooledCache; - } - function pushTransition(offscreenWorkInProgress, prevCachePool) { - null === prevCachePool ? push2(resumedCache, resumedCache.current) : push2(resumedCache, prevCachePool.pool); - } - function getSuspendedCache() { - var cacheFromPool = peekCacheFromPool(); - return null === cacheFromPool ? null : { parent: CacheContext._currentValue, pool: cacheFromPool }; - } - var SuspenseException = Error(formatProdErrorMessage(460)), SuspenseyCommitException = Error(formatProdErrorMessage(474)), SuspenseActionException = Error(formatProdErrorMessage(542)), noopSuspenseyCommitThenable = { then: function() { - } }; - function isThenableResolved(thenable) { - thenable = thenable.status; - return "fulfilled" === thenable || "rejected" === thenable; - } - function trackUsedThenable(thenableState2, thenable, index2) { - index2 = thenableState2[index2]; - void 0 === index2 ? thenableState2.push(thenable) : index2 !== thenable && (thenable.then(noop$12, noop$12), thenable = index2); - switch (thenable.status) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenableState2 = thenable.reason, checkIfUseWrappedInAsyncCatch(thenableState2), thenableState2; - default: - if ("string" === typeof thenable.status) thenable.then(noop$12, noop$12); - else { - thenableState2 = workInProgressRoot; - if (null !== thenableState2 && 100 < thenableState2.shellSuspendCounter) - throw Error(formatProdErrorMessage(482)); - thenableState2 = thenable; - thenableState2.status = "pending"; - thenableState2.then( - function(fulfilledValue) { - if ("pending" === thenable.status) { - var fulfilledThenable = thenable; - fulfilledThenable.status = "fulfilled"; - fulfilledThenable.value = fulfilledValue; - } - }, - function(error2) { - if ("pending" === thenable.status) { - var rejectedThenable = thenable; - rejectedThenable.status = "rejected"; - rejectedThenable.reason = error2; - } - } - ); - } - switch (thenable.status) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenableState2 = thenable.reason, checkIfUseWrappedInAsyncCatch(thenableState2), thenableState2; - } - suspendedThenable = thenable; - throw SuspenseException; - } - } - function resolveLazy(lazyType) { - try { - var init = lazyType._init; - return init(lazyType._payload); - } catch (x2) { - if (null !== x2 && "object" === typeof x2 && "function" === typeof x2.then) - throw suspendedThenable = x2, SuspenseException; - throw x2; - } - } - var suspendedThenable = null; - function getSuspendedThenable() { - if (null === suspendedThenable) throw Error(formatProdErrorMessage(459)); - var thenable = suspendedThenable; - suspendedThenable = null; - return thenable; - } - function checkIfUseWrappedInAsyncCatch(rejectedReason) { - if (rejectedReason === SuspenseException || rejectedReason === SuspenseActionException) - throw Error(formatProdErrorMessage(483)); - } - var thenableState$1 = null, thenableIndexCounter$1 = 0; - function unwrapThenable(thenable) { - var index2 = thenableIndexCounter$1; - thenableIndexCounter$1 += 1; - null === thenableState$1 && (thenableState$1 = []); - return trackUsedThenable(thenableState$1, thenable, index2); - } - function coerceRef(workInProgress2, element) { - element = element.props.ref; - workInProgress2.ref = void 0 !== element ? element : null; - } - function throwOnInvalidObjectTypeImpl(returnFiber, newChild) { - if (newChild.$$typeof === REACT_LEGACY_ELEMENT_TYPE) - throw Error(formatProdErrorMessage(525)); - returnFiber = Object.prototype.toString.call(newChild); - throw Error( - formatProdErrorMessage( - 31, - "[object Object]" === returnFiber ? "object with keys {" + Object.keys(newChild).join(", ") + "}" : returnFiber - ) - ); - } - function createChildReconciler(shouldTrackSideEffects) { - function deleteChild(returnFiber, childToDelete) { - if (shouldTrackSideEffects) { - var deletions = returnFiber.deletions; - null === deletions ? (returnFiber.deletions = [childToDelete], returnFiber.flags |= 16) : deletions.push(childToDelete); - } - } - function deleteRemainingChildren(returnFiber, currentFirstChild) { - if (!shouldTrackSideEffects) return null; - for (; null !== currentFirstChild; ) - deleteChild(returnFiber, currentFirstChild), currentFirstChild = currentFirstChild.sibling; - return null; - } - function mapRemainingChildren(currentFirstChild) { - for (var existingChildren = /* @__PURE__ */ new Map(); null !== currentFirstChild; ) - null !== currentFirstChild.key ? existingChildren.set(currentFirstChild.key, currentFirstChild) : existingChildren.set(currentFirstChild.index, currentFirstChild), currentFirstChild = currentFirstChild.sibling; - return existingChildren; - } - function useFiber(fiber, pendingProps) { - fiber = createWorkInProgress(fiber, pendingProps); - fiber.index = 0; - fiber.sibling = null; - return fiber; - } - function placeChild(newFiber, lastPlacedIndex, newIndex) { - newFiber.index = newIndex; - if (!shouldTrackSideEffects) - return newFiber.flags |= 1048576, lastPlacedIndex; - newIndex = newFiber.alternate; - if (null !== newIndex) - return newIndex = newIndex.index, newIndex < lastPlacedIndex ? (newFiber.flags |= 67108866, lastPlacedIndex) : newIndex; - newFiber.flags |= 67108866; - return lastPlacedIndex; - } - function placeSingleChild(newFiber) { - shouldTrackSideEffects && null === newFiber.alternate && (newFiber.flags |= 67108866); - return newFiber; - } - function updateTextNode(returnFiber, current3, textContent, lanes) { - if (null === current3 || 6 !== current3.tag) - return current3 = createFiberFromText(textContent, returnFiber.mode, lanes), current3.return = returnFiber, current3; - current3 = useFiber(current3, textContent); - current3.return = returnFiber; - return current3; - } - function updateElement(returnFiber, current3, element, lanes) { - var elementType = element.type; - if (elementType === REACT_FRAGMENT_TYPE) - return updateFragment( - returnFiber, - current3, - element.props.children, - lanes, - element.key - ); - if (null !== current3 && (current3.elementType === elementType || "object" === typeof elementType && null !== elementType && elementType.$$typeof === REACT_LAZY_TYPE && resolveLazy(elementType) === current3.type)) - return current3 = useFiber(current3, element.props), coerceRef(current3, element), current3.return = returnFiber, current3; - current3 = createFiberFromTypeAndProps( - element.type, - element.key, - element.props, - null, - returnFiber.mode, - lanes - ); - coerceRef(current3, element); - current3.return = returnFiber; - return current3; - } - function updatePortal(returnFiber, current3, portal, lanes) { - if (null === current3 || 4 !== current3.tag || current3.stateNode.containerInfo !== portal.containerInfo || current3.stateNode.implementation !== portal.implementation) - return current3 = createFiberFromPortal(portal, returnFiber.mode, lanes), current3.return = returnFiber, current3; - current3 = useFiber(current3, portal.children || []); - current3.return = returnFiber; - return current3; - } - function updateFragment(returnFiber, current3, fragment, lanes, key2) { - if (null === current3 || 7 !== current3.tag) - return current3 = createFiberFromFragment( - fragment, - returnFiber.mode, - lanes, - key2 - ), current3.return = returnFiber, current3; - current3 = useFiber(current3, fragment); - current3.return = returnFiber; - return current3; - } - function createChild(returnFiber, newChild, lanes) { - if ("string" === typeof newChild && "" !== newChild || "number" === typeof newChild || "bigint" === typeof newChild) - return newChild = createFiberFromText( - "" + newChild, - returnFiber.mode, - lanes - ), newChild.return = returnFiber, newChild; - if ("object" === typeof newChild && null !== newChild) { - switch (newChild.$$typeof) { - case REACT_ELEMENT_TYPE: - return lanes = createFiberFromTypeAndProps( - newChild.type, - newChild.key, - newChild.props, - null, - returnFiber.mode, - lanes - ), coerceRef(lanes, newChild), lanes.return = returnFiber, lanes; - case REACT_PORTAL_TYPE: - return newChild = createFiberFromPortal( - newChild, - returnFiber.mode, - lanes - ), newChild.return = returnFiber, newChild; - case REACT_LAZY_TYPE: - return newChild = resolveLazy(newChild), createChild(returnFiber, newChild, lanes); - } - if (isArrayImpl(newChild) || getIteratorFn(newChild)) - return newChild = createFiberFromFragment( - newChild, - returnFiber.mode, - lanes, - null - ), newChild.return = returnFiber, newChild; - if ("function" === typeof newChild.then) - return createChild(returnFiber, unwrapThenable(newChild), lanes); - if (newChild.$$typeof === REACT_CONTEXT_TYPE) - return createChild( - returnFiber, - readContextDuringReconciliation(returnFiber, newChild), - lanes - ); - throwOnInvalidObjectTypeImpl(returnFiber, newChild); - } - return null; - } - function updateSlot(returnFiber, oldFiber, newChild, lanes) { - var key2 = null !== oldFiber ? oldFiber.key : null; - if ("string" === typeof newChild && "" !== newChild || "number" === typeof newChild || "bigint" === typeof newChild) - return null !== key2 ? null : updateTextNode(returnFiber, oldFiber, "" + newChild, lanes); - if ("object" === typeof newChild && null !== newChild) { - switch (newChild.$$typeof) { - case REACT_ELEMENT_TYPE: - return newChild.key === key2 ? updateElement(returnFiber, oldFiber, newChild, lanes) : null; - case REACT_PORTAL_TYPE: - return newChild.key === key2 ? updatePortal(returnFiber, oldFiber, newChild, lanes) : null; - case REACT_LAZY_TYPE: - return newChild = resolveLazy(newChild), updateSlot(returnFiber, oldFiber, newChild, lanes); - } - if (isArrayImpl(newChild) || getIteratorFn(newChild)) - return null !== key2 ? null : updateFragment(returnFiber, oldFiber, newChild, lanes, null); - if ("function" === typeof newChild.then) - return updateSlot( - returnFiber, - oldFiber, - unwrapThenable(newChild), - lanes - ); - if (newChild.$$typeof === REACT_CONTEXT_TYPE) - return updateSlot( - returnFiber, - oldFiber, - readContextDuringReconciliation(returnFiber, newChild), - lanes - ); - throwOnInvalidObjectTypeImpl(returnFiber, newChild); - } - return null; - } - function updateFromMap(existingChildren, returnFiber, newIdx, newChild, lanes) { - if ("string" === typeof newChild && "" !== newChild || "number" === typeof newChild || "bigint" === typeof newChild) - return existingChildren = existingChildren.get(newIdx) || null, updateTextNode(returnFiber, existingChildren, "" + newChild, lanes); - if ("object" === typeof newChild && null !== newChild) { - switch (newChild.$$typeof) { - case REACT_ELEMENT_TYPE: - return existingChildren = existingChildren.get( - null === newChild.key ? newIdx : newChild.key - ) || null, updateElement(returnFiber, existingChildren, newChild, lanes); - case REACT_PORTAL_TYPE: - return existingChildren = existingChildren.get( - null === newChild.key ? newIdx : newChild.key - ) || null, updatePortal(returnFiber, existingChildren, newChild, lanes); - case REACT_LAZY_TYPE: - return newChild = resolveLazy(newChild), updateFromMap( - existingChildren, - returnFiber, - newIdx, - newChild, - lanes - ); - } - if (isArrayImpl(newChild) || getIteratorFn(newChild)) - return existingChildren = existingChildren.get(newIdx) || null, updateFragment(returnFiber, existingChildren, newChild, lanes, null); - if ("function" === typeof newChild.then) - return updateFromMap( - existingChildren, - returnFiber, - newIdx, - unwrapThenable(newChild), - lanes - ); - if (newChild.$$typeof === REACT_CONTEXT_TYPE) - return updateFromMap( - existingChildren, - returnFiber, - newIdx, - readContextDuringReconciliation(returnFiber, newChild), - lanes - ); - throwOnInvalidObjectTypeImpl(returnFiber, newChild); - } - return null; - } - function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) { - for (var resultingFirstChild = null, previousNewFiber = null, oldFiber = currentFirstChild, newIdx = currentFirstChild = 0, nextOldFiber = null; null !== oldFiber && newIdx < newChildren.length; newIdx++) { - oldFiber.index > newIdx ? (nextOldFiber = oldFiber, oldFiber = null) : nextOldFiber = oldFiber.sibling; - var newFiber = updateSlot( - returnFiber, - oldFiber, - newChildren[newIdx], - lanes - ); - if (null === newFiber) { - null === oldFiber && (oldFiber = nextOldFiber); - break; - } - shouldTrackSideEffects && oldFiber && null === newFiber.alternate && deleteChild(returnFiber, oldFiber); - currentFirstChild = placeChild(newFiber, currentFirstChild, newIdx); - null === previousNewFiber ? resultingFirstChild = newFiber : previousNewFiber.sibling = newFiber; - previousNewFiber = newFiber; - oldFiber = nextOldFiber; - } - if (newIdx === newChildren.length) - return deleteRemainingChildren(returnFiber, oldFiber), isHydrating && pushTreeFork(returnFiber, newIdx), resultingFirstChild; - if (null === oldFiber) { - for (; newIdx < newChildren.length; newIdx++) - oldFiber = createChild(returnFiber, newChildren[newIdx], lanes), null !== oldFiber && (currentFirstChild = placeChild( - oldFiber, - currentFirstChild, - newIdx - ), null === previousNewFiber ? resultingFirstChild = oldFiber : previousNewFiber.sibling = oldFiber, previousNewFiber = oldFiber); - isHydrating && pushTreeFork(returnFiber, newIdx); - return resultingFirstChild; - } - for (oldFiber = mapRemainingChildren(oldFiber); newIdx < newChildren.length; newIdx++) - nextOldFiber = updateFromMap( - oldFiber, - returnFiber, - newIdx, - newChildren[newIdx], - lanes - ), null !== nextOldFiber && (shouldTrackSideEffects && null !== nextOldFiber.alternate && oldFiber.delete( - null === nextOldFiber.key ? newIdx : nextOldFiber.key - ), currentFirstChild = placeChild( - nextOldFiber, - currentFirstChild, - newIdx - ), null === previousNewFiber ? resultingFirstChild = nextOldFiber : previousNewFiber.sibling = nextOldFiber, previousNewFiber = nextOldFiber); - shouldTrackSideEffects && oldFiber.forEach(function(child) { - return deleteChild(returnFiber, child); - }); - isHydrating && pushTreeFork(returnFiber, newIdx); - return resultingFirstChild; - } - function reconcileChildrenIterator(returnFiber, currentFirstChild, newChildren, lanes) { - if (null == newChildren) throw Error(formatProdErrorMessage(151)); - for (var resultingFirstChild = null, previousNewFiber = null, oldFiber = currentFirstChild, newIdx = currentFirstChild = 0, nextOldFiber = null, step = newChildren.next(); null !== oldFiber && !step.done; newIdx++, step = newChildren.next()) { - oldFiber.index > newIdx ? (nextOldFiber = oldFiber, oldFiber = null) : nextOldFiber = oldFiber.sibling; - var newFiber = updateSlot(returnFiber, oldFiber, step.value, lanes); - if (null === newFiber) { - null === oldFiber && (oldFiber = nextOldFiber); - break; - } - shouldTrackSideEffects && oldFiber && null === newFiber.alternate && deleteChild(returnFiber, oldFiber); - currentFirstChild = placeChild(newFiber, currentFirstChild, newIdx); - null === previousNewFiber ? resultingFirstChild = newFiber : previousNewFiber.sibling = newFiber; - previousNewFiber = newFiber; - oldFiber = nextOldFiber; - } - if (step.done) - return deleteRemainingChildren(returnFiber, oldFiber), isHydrating && pushTreeFork(returnFiber, newIdx), resultingFirstChild; - if (null === oldFiber) { - for (; !step.done; newIdx++, step = newChildren.next()) - step = createChild(returnFiber, step.value, lanes), null !== step && (currentFirstChild = placeChild(step, currentFirstChild, newIdx), null === previousNewFiber ? resultingFirstChild = step : previousNewFiber.sibling = step, previousNewFiber = step); - isHydrating && pushTreeFork(returnFiber, newIdx); - return resultingFirstChild; - } - for (oldFiber = mapRemainingChildren(oldFiber); !step.done; newIdx++, step = newChildren.next()) - step = updateFromMap(oldFiber, returnFiber, newIdx, step.value, lanes), null !== step && (shouldTrackSideEffects && null !== step.alternate && oldFiber.delete(null === step.key ? newIdx : step.key), currentFirstChild = placeChild(step, currentFirstChild, newIdx), null === previousNewFiber ? resultingFirstChild = step : previousNewFiber.sibling = step, previousNewFiber = step); - shouldTrackSideEffects && oldFiber.forEach(function(child) { - return deleteChild(returnFiber, child); - }); - isHydrating && pushTreeFork(returnFiber, newIdx); - return resultingFirstChild; - } - function reconcileChildFibersImpl(returnFiber, currentFirstChild, newChild, lanes) { - "object" === typeof newChild && null !== newChild && newChild.type === REACT_FRAGMENT_TYPE && null === newChild.key && (newChild = newChild.props.children); - if ("object" === typeof newChild && null !== newChild) { - switch (newChild.$$typeof) { - case REACT_ELEMENT_TYPE: - a: { - for (var key2 = newChild.key; null !== currentFirstChild; ) { - if (currentFirstChild.key === key2) { - key2 = newChild.type; - if (key2 === REACT_FRAGMENT_TYPE) { - if (7 === currentFirstChild.tag) { - deleteRemainingChildren( - returnFiber, - currentFirstChild.sibling - ); - lanes = useFiber( - currentFirstChild, - newChild.props.children - ); - lanes.return = returnFiber; - returnFiber = lanes; - break a; - } - } else if (currentFirstChild.elementType === key2 || "object" === typeof key2 && null !== key2 && key2.$$typeof === REACT_LAZY_TYPE && resolveLazy(key2) === currentFirstChild.type) { - deleteRemainingChildren( - returnFiber, - currentFirstChild.sibling - ); - lanes = useFiber(currentFirstChild, newChild.props); - coerceRef(lanes, newChild); - lanes.return = returnFiber; - returnFiber = lanes; - break a; - } - deleteRemainingChildren(returnFiber, currentFirstChild); - break; - } else deleteChild(returnFiber, currentFirstChild); - currentFirstChild = currentFirstChild.sibling; - } - newChild.type === REACT_FRAGMENT_TYPE ? (lanes = createFiberFromFragment( - newChild.props.children, - returnFiber.mode, - lanes, - newChild.key - ), lanes.return = returnFiber, returnFiber = lanes) : (lanes = createFiberFromTypeAndProps( - newChild.type, - newChild.key, - newChild.props, - null, - returnFiber.mode, - lanes - ), coerceRef(lanes, newChild), lanes.return = returnFiber, returnFiber = lanes); - } - return placeSingleChild(returnFiber); - case REACT_PORTAL_TYPE: - a: { - for (key2 = newChild.key; null !== currentFirstChild; ) { - if (currentFirstChild.key === key2) - if (4 === currentFirstChild.tag && currentFirstChild.stateNode.containerInfo === newChild.containerInfo && currentFirstChild.stateNode.implementation === newChild.implementation) { - deleteRemainingChildren( - returnFiber, - currentFirstChild.sibling - ); - lanes = useFiber(currentFirstChild, newChild.children || []); - lanes.return = returnFiber; - returnFiber = lanes; - break a; - } else { - deleteRemainingChildren(returnFiber, currentFirstChild); - break; - } - else deleteChild(returnFiber, currentFirstChild); - currentFirstChild = currentFirstChild.sibling; - } - lanes = createFiberFromPortal(newChild, returnFiber.mode, lanes); - lanes.return = returnFiber; - returnFiber = lanes; - } - return placeSingleChild(returnFiber); - case REACT_LAZY_TYPE: - return newChild = resolveLazy(newChild), reconcileChildFibersImpl( - returnFiber, - currentFirstChild, - newChild, - lanes - ); - } - if (isArrayImpl(newChild)) - return reconcileChildrenArray( - returnFiber, - currentFirstChild, - newChild, - lanes - ); - if (getIteratorFn(newChild)) { - key2 = getIteratorFn(newChild); - if ("function" !== typeof key2) throw Error(formatProdErrorMessage(150)); - newChild = key2.call(newChild); - return reconcileChildrenIterator( - returnFiber, - currentFirstChild, - newChild, - lanes - ); - } - if ("function" === typeof newChild.then) - return reconcileChildFibersImpl( - returnFiber, - currentFirstChild, - unwrapThenable(newChild), - lanes - ); - if (newChild.$$typeof === REACT_CONTEXT_TYPE) - return reconcileChildFibersImpl( - returnFiber, - currentFirstChild, - readContextDuringReconciliation(returnFiber, newChild), - lanes - ); - throwOnInvalidObjectTypeImpl(returnFiber, newChild); - } - return "string" === typeof newChild && "" !== newChild || "number" === typeof newChild || "bigint" === typeof newChild ? (newChild = "" + newChild, null !== currentFirstChild && 6 === currentFirstChild.tag ? (deleteRemainingChildren(returnFiber, currentFirstChild.sibling), lanes = useFiber(currentFirstChild, newChild), lanes.return = returnFiber, returnFiber = lanes) : (deleteRemainingChildren(returnFiber, currentFirstChild), lanes = createFiberFromText(newChild, returnFiber.mode, lanes), lanes.return = returnFiber, returnFiber = lanes), placeSingleChild(returnFiber)) : deleteRemainingChildren(returnFiber, currentFirstChild); - } - return function(returnFiber, currentFirstChild, newChild, lanes) { - try { - thenableIndexCounter$1 = 0; - var firstChildFiber = reconcileChildFibersImpl( - returnFiber, - currentFirstChild, - newChild, - lanes - ); - thenableState$1 = null; - return firstChildFiber; - } catch (x2) { - if (x2 === SuspenseException || x2 === SuspenseActionException) throw x2; - var fiber = createFiberImplClass(29, x2, null, returnFiber.mode); - fiber.lanes = lanes; - fiber.return = returnFiber; - return fiber; - } finally { - } - }; - } - var reconcileChildFibers = createChildReconciler(true), mountChildFibers = createChildReconciler(false), hasForceUpdate = false; - function initializeUpdateQueue(fiber) { - fiber.updateQueue = { - baseState: fiber.memoizedState, - firstBaseUpdate: null, - lastBaseUpdate: null, - shared: { pending: null, lanes: 0, hiddenCallbacks: null }, - callbacks: null - }; - } - function cloneUpdateQueue(current3, workInProgress2) { - current3 = current3.updateQueue; - workInProgress2.updateQueue === current3 && (workInProgress2.updateQueue = { - baseState: current3.baseState, - firstBaseUpdate: current3.firstBaseUpdate, - lastBaseUpdate: current3.lastBaseUpdate, - shared: current3.shared, - callbacks: null - }); - } - function createUpdate(lane) { - return { lane, tag: 0, payload: null, callback: null, next: null }; - } - function enqueueUpdate(fiber, update2, lane) { - var updateQueue = fiber.updateQueue; - if (null === updateQueue) return null; - updateQueue = updateQueue.shared; - if (0 !== (executionContext & 2)) { - var pending = updateQueue.pending; - null === pending ? update2.next = update2 : (update2.next = pending.next, pending.next = update2); - updateQueue.pending = update2; - update2 = getRootForUpdatedFiber(fiber); - markUpdateLaneFromFiberToRoot(fiber, null, lane); - return update2; - } - enqueueUpdate$1(fiber, updateQueue, update2, lane); - return getRootForUpdatedFiber(fiber); - } - function entangleTransitions(root3, fiber, lane) { - fiber = fiber.updateQueue; - if (null !== fiber && (fiber = fiber.shared, 0 !== (lane & 4194048))) { - var queueLanes = fiber.lanes; - queueLanes &= root3.pendingLanes; - lane |= queueLanes; - fiber.lanes = lane; - markRootEntangled(root3, lane); - } - } - function enqueueCapturedUpdate(workInProgress2, capturedUpdate) { - var queue = workInProgress2.updateQueue, current3 = workInProgress2.alternate; - if (null !== current3 && (current3 = current3.updateQueue, queue === current3)) { - var newFirst = null, newLast = null; - queue = queue.firstBaseUpdate; - if (null !== queue) { - do { - var clone2 = { - lane: queue.lane, - tag: queue.tag, - payload: queue.payload, - callback: null, - next: null - }; - null === newLast ? newFirst = newLast = clone2 : newLast = newLast.next = clone2; - queue = queue.next; - } while (null !== queue); - null === newLast ? newFirst = newLast = capturedUpdate : newLast = newLast.next = capturedUpdate; - } else newFirst = newLast = capturedUpdate; - queue = { - baseState: current3.baseState, - firstBaseUpdate: newFirst, - lastBaseUpdate: newLast, - shared: current3.shared, - callbacks: current3.callbacks - }; - workInProgress2.updateQueue = queue; - return; - } - workInProgress2 = queue.lastBaseUpdate; - null === workInProgress2 ? queue.firstBaseUpdate = capturedUpdate : workInProgress2.next = capturedUpdate; - queue.lastBaseUpdate = capturedUpdate; - } - var didReadFromEntangledAsyncAction = false; - function suspendIfUpdateReadFromEntangledAsyncAction() { - if (didReadFromEntangledAsyncAction) { - var entangledActionThenable = currentEntangledActionThenable; - if (null !== entangledActionThenable) throw entangledActionThenable; - } - } - function processUpdateQueue(workInProgress$jscomp$0, props, instance$jscomp$0, renderLanes2) { - didReadFromEntangledAsyncAction = false; - var queue = workInProgress$jscomp$0.updateQueue; - hasForceUpdate = false; - var firstBaseUpdate = queue.firstBaseUpdate, lastBaseUpdate = queue.lastBaseUpdate, pendingQueue = queue.shared.pending; - if (null !== pendingQueue) { - queue.shared.pending = null; - var lastPendingUpdate = pendingQueue, firstPendingUpdate = lastPendingUpdate.next; - lastPendingUpdate.next = null; - null === lastBaseUpdate ? firstBaseUpdate = firstPendingUpdate : lastBaseUpdate.next = firstPendingUpdate; - lastBaseUpdate = lastPendingUpdate; - var current3 = workInProgress$jscomp$0.alternate; - null !== current3 && (current3 = current3.updateQueue, pendingQueue = current3.lastBaseUpdate, pendingQueue !== lastBaseUpdate && (null === pendingQueue ? current3.firstBaseUpdate = firstPendingUpdate : pendingQueue.next = firstPendingUpdate, current3.lastBaseUpdate = lastPendingUpdate)); - } - if (null !== firstBaseUpdate) { - var newState = queue.baseState; - lastBaseUpdate = 0; - current3 = firstPendingUpdate = lastPendingUpdate = null; - pendingQueue = firstBaseUpdate; - do { - var updateLane = pendingQueue.lane & -536870913, isHiddenUpdate = updateLane !== pendingQueue.lane; - if (isHiddenUpdate ? (workInProgressRootRenderLanes & updateLane) === updateLane : (renderLanes2 & updateLane) === updateLane) { - 0 !== updateLane && updateLane === currentEntangledLane && (didReadFromEntangledAsyncAction = true); - null !== current3 && (current3 = current3.next = { - lane: 0, - tag: pendingQueue.tag, - payload: pendingQueue.payload, - callback: null, - next: null - }); - a: { - var workInProgress2 = workInProgress$jscomp$0, update2 = pendingQueue; - updateLane = props; - var instance2 = instance$jscomp$0; - switch (update2.tag) { - case 1: - workInProgress2 = update2.payload; - if ("function" === typeof workInProgress2) { - newState = workInProgress2.call(instance2, newState, updateLane); - break a; - } - newState = workInProgress2; - break a; - case 3: - workInProgress2.flags = workInProgress2.flags & -65537 | 128; - case 0: - workInProgress2 = update2.payload; - updateLane = "function" === typeof workInProgress2 ? workInProgress2.call(instance2, newState, updateLane) : workInProgress2; - if (null === updateLane || void 0 === updateLane) break a; - newState = assign2({}, newState, updateLane); - break a; - case 2: - hasForceUpdate = true; - } - } - updateLane = pendingQueue.callback; - null !== updateLane && (workInProgress$jscomp$0.flags |= 64, isHiddenUpdate && (workInProgress$jscomp$0.flags |= 8192), isHiddenUpdate = queue.callbacks, null === isHiddenUpdate ? queue.callbacks = [updateLane] : isHiddenUpdate.push(updateLane)); - } else - isHiddenUpdate = { - lane: updateLane, - tag: pendingQueue.tag, - payload: pendingQueue.payload, - callback: pendingQueue.callback, - next: null - }, null === current3 ? (firstPendingUpdate = current3 = isHiddenUpdate, lastPendingUpdate = newState) : current3 = current3.next = isHiddenUpdate, lastBaseUpdate |= updateLane; - pendingQueue = pendingQueue.next; - if (null === pendingQueue) - if (pendingQueue = queue.shared.pending, null === pendingQueue) - break; - else - isHiddenUpdate = pendingQueue, pendingQueue = isHiddenUpdate.next, isHiddenUpdate.next = null, queue.lastBaseUpdate = isHiddenUpdate, queue.shared.pending = null; - } while (1); - null === current3 && (lastPendingUpdate = newState); - queue.baseState = lastPendingUpdate; - queue.firstBaseUpdate = firstPendingUpdate; - queue.lastBaseUpdate = current3; - null === firstBaseUpdate && (queue.shared.lanes = 0); - workInProgressRootSkippedLanes |= lastBaseUpdate; - workInProgress$jscomp$0.lanes = lastBaseUpdate; - workInProgress$jscomp$0.memoizedState = newState; - } - } - function callCallback(callback, context) { - if ("function" !== typeof callback) - throw Error(formatProdErrorMessage(191, callback)); - callback.call(context); - } - function commitCallbacks(updateQueue, context) { - var callbacks = updateQueue.callbacks; - if (null !== callbacks) - for (updateQueue.callbacks = null, updateQueue = 0; updateQueue < callbacks.length; updateQueue++) - callCallback(callbacks[updateQueue], context); - } - var currentTreeHiddenStackCursor = createCursor(null), prevEntangledRenderLanesCursor = createCursor(0); - function pushHiddenContext(fiber, context) { - fiber = entangledRenderLanes; - push2(prevEntangledRenderLanesCursor, fiber); - push2(currentTreeHiddenStackCursor, context); - entangledRenderLanes = fiber | context.baseLanes; - } - function reuseHiddenContextOnStack() { - push2(prevEntangledRenderLanesCursor, entangledRenderLanes); - push2(currentTreeHiddenStackCursor, currentTreeHiddenStackCursor.current); - } - function popHiddenContext() { - entangledRenderLanes = prevEntangledRenderLanesCursor.current; - pop2(currentTreeHiddenStackCursor); - pop2(prevEntangledRenderLanesCursor); - } - var suspenseHandlerStackCursor = createCursor(null), shellBoundary = null; - function pushPrimaryTreeSuspenseHandler(handler) { - var current3 = handler.alternate; - push2(suspenseStackCursor, suspenseStackCursor.current & 1); - push2(suspenseHandlerStackCursor, handler); - null === shellBoundary && (null === current3 || null !== currentTreeHiddenStackCursor.current ? shellBoundary = handler : null !== current3.memoizedState && (shellBoundary = handler)); - } - function pushDehydratedActivitySuspenseHandler(fiber) { - push2(suspenseStackCursor, suspenseStackCursor.current); - push2(suspenseHandlerStackCursor, fiber); - null === shellBoundary && (shellBoundary = fiber); - } - function pushOffscreenSuspenseHandler(fiber) { - 22 === fiber.tag ? (push2(suspenseStackCursor, suspenseStackCursor.current), push2(suspenseHandlerStackCursor, fiber), null === shellBoundary && (shellBoundary = fiber)) : reuseSuspenseHandlerOnStack(); - } - function reuseSuspenseHandlerOnStack() { - push2(suspenseStackCursor, suspenseStackCursor.current); - push2(suspenseHandlerStackCursor, suspenseHandlerStackCursor.current); - } - function popSuspenseHandler(fiber) { - pop2(suspenseHandlerStackCursor); - shellBoundary === fiber && (shellBoundary = null); - pop2(suspenseStackCursor); - } - var suspenseStackCursor = createCursor(0); - function findFirstSuspended(row2) { - for (var node2 = row2; null !== node2; ) { - if (13 === node2.tag) { - var state = node2.memoizedState; - if (null !== state && (state = state.dehydrated, null === state || isSuspenseInstancePending(state) || isSuspenseInstanceFallback(state))) - return node2; - } else if (19 === node2.tag && ("forwards" === node2.memoizedProps.revealOrder || "backwards" === node2.memoizedProps.revealOrder || "unstable_legacy-backwards" === node2.memoizedProps.revealOrder || "together" === node2.memoizedProps.revealOrder)) { - if (0 !== (node2.flags & 128)) return node2; - } else if (null !== node2.child) { - node2.child.return = node2; - node2 = node2.child; - continue; - } - if (node2 === row2) break; - for (; null === node2.sibling; ) { - if (null === node2.return || node2.return === row2) return null; - node2 = node2.return; - } - node2.sibling.return = node2.return; - node2 = node2.sibling; - } - return null; - } - var renderLanes = 0, currentlyRenderingFiber = null, currentHook = null, workInProgressHook = null, didScheduleRenderPhaseUpdate = false, didScheduleRenderPhaseUpdateDuringThisPass = false, shouldDoubleInvokeUserFnsInHooksDEV = false, localIdCounter = 0, thenableIndexCounter = 0, thenableState = null, globalClientIdCounter = 0; - function throwInvalidHookError() { - throw Error(formatProdErrorMessage(321)); - } - function areHookInputsEqual(nextDeps, prevDeps) { - if (null === prevDeps) return false; - for (var i4 = 0; i4 < prevDeps.length && i4 < nextDeps.length; i4++) - if (!objectIs(nextDeps[i4], prevDeps[i4])) return false; - return true; - } - function renderWithHooks(current3, workInProgress2, Component2, props, secondArg, nextRenderLanes) { - renderLanes = nextRenderLanes; - currentlyRenderingFiber = workInProgress2; - workInProgress2.memoizedState = null; - workInProgress2.updateQueue = null; - workInProgress2.lanes = 0; - ReactSharedInternals.H = null === current3 || null === current3.memoizedState ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; - shouldDoubleInvokeUserFnsInHooksDEV = false; - nextRenderLanes = Component2(props, secondArg); - shouldDoubleInvokeUserFnsInHooksDEV = false; - didScheduleRenderPhaseUpdateDuringThisPass && (nextRenderLanes = renderWithHooksAgain( - workInProgress2, - Component2, - props, - secondArg - )); - finishRenderingHooks(current3); - return nextRenderLanes; - } - function finishRenderingHooks(current3) { - ReactSharedInternals.H = ContextOnlyDispatcher; - var didRenderTooFewHooks = null !== currentHook && null !== currentHook.next; - renderLanes = 0; - workInProgressHook = currentHook = currentlyRenderingFiber = null; - didScheduleRenderPhaseUpdate = false; - thenableIndexCounter = 0; - thenableState = null; - if (didRenderTooFewHooks) throw Error(formatProdErrorMessage(300)); - null === current3 || didReceiveUpdate || (current3 = current3.dependencies, null !== current3 && checkIfContextChanged(current3) && (didReceiveUpdate = true)); - } - function renderWithHooksAgain(workInProgress2, Component2, props, secondArg) { - currentlyRenderingFiber = workInProgress2; - var numberOfReRenders = 0; - do { - didScheduleRenderPhaseUpdateDuringThisPass && (thenableState = null); - thenableIndexCounter = 0; - didScheduleRenderPhaseUpdateDuringThisPass = false; - if (25 <= numberOfReRenders) throw Error(formatProdErrorMessage(301)); - numberOfReRenders += 1; - workInProgressHook = currentHook = null; - if (null != workInProgress2.updateQueue) { - var children2 = workInProgress2.updateQueue; - children2.lastEffect = null; - children2.events = null; - children2.stores = null; - null != children2.memoCache && (children2.memoCache.index = 0); - } - ReactSharedInternals.H = HooksDispatcherOnRerender; - children2 = Component2(props, secondArg); - } while (didScheduleRenderPhaseUpdateDuringThisPass); - return children2; - } - function TransitionAwareHostComponent() { - var dispatcher = ReactSharedInternals.H, maybeThenable = dispatcher.useState()[0]; - maybeThenable = "function" === typeof maybeThenable.then ? useThenable(maybeThenable) : maybeThenable; - dispatcher = dispatcher.useState()[0]; - (null !== currentHook ? currentHook.memoizedState : null) !== dispatcher && (currentlyRenderingFiber.flags |= 1024); - return maybeThenable; - } - function checkDidRenderIdHook() { - var didRenderIdHook = 0 !== localIdCounter; - localIdCounter = 0; - return didRenderIdHook; - } - function bailoutHooks(current3, workInProgress2, lanes) { - workInProgress2.updateQueue = current3.updateQueue; - workInProgress2.flags &= -2053; - current3.lanes &= ~lanes; - } - function resetHooksOnUnwind(workInProgress2) { - if (didScheduleRenderPhaseUpdate) { - for (workInProgress2 = workInProgress2.memoizedState; null !== workInProgress2; ) { - var queue = workInProgress2.queue; - null !== queue && (queue.pending = null); - workInProgress2 = workInProgress2.next; - } - didScheduleRenderPhaseUpdate = false; - } - renderLanes = 0; - workInProgressHook = currentHook = currentlyRenderingFiber = null; - didScheduleRenderPhaseUpdateDuringThisPass = false; - thenableIndexCounter = localIdCounter = 0; - thenableState = null; - } - function mountWorkInProgressHook() { - var hook = { - memoizedState: null, - baseState: null, - baseQueue: null, - queue: null, - next: null - }; - null === workInProgressHook ? currentlyRenderingFiber.memoizedState = workInProgressHook = hook : workInProgressHook = workInProgressHook.next = hook; - return workInProgressHook; - } - function updateWorkInProgressHook() { - if (null === currentHook) { - var nextCurrentHook = currentlyRenderingFiber.alternate; - nextCurrentHook = null !== nextCurrentHook ? nextCurrentHook.memoizedState : null; - } else nextCurrentHook = currentHook.next; - var nextWorkInProgressHook = null === workInProgressHook ? currentlyRenderingFiber.memoizedState : workInProgressHook.next; - if (null !== nextWorkInProgressHook) - workInProgressHook = nextWorkInProgressHook, currentHook = nextCurrentHook; - else { - if (null === nextCurrentHook) { - if (null === currentlyRenderingFiber.alternate) - throw Error(formatProdErrorMessage(467)); - throw Error(formatProdErrorMessage(310)); - } - currentHook = nextCurrentHook; - nextCurrentHook = { - memoizedState: currentHook.memoizedState, - baseState: currentHook.baseState, - baseQueue: currentHook.baseQueue, - queue: currentHook.queue, - next: null - }; - null === workInProgressHook ? currentlyRenderingFiber.memoizedState = workInProgressHook = nextCurrentHook : workInProgressHook = workInProgressHook.next = nextCurrentHook; - } - return workInProgressHook; - } - function createFunctionComponentUpdateQueue() { - return { lastEffect: null, events: null, stores: null, memoCache: null }; - } - function useThenable(thenable) { - var index2 = thenableIndexCounter; - thenableIndexCounter += 1; - null === thenableState && (thenableState = []); - thenable = trackUsedThenable(thenableState, thenable, index2); - index2 = currentlyRenderingFiber; - null === (null === workInProgressHook ? index2.memoizedState : workInProgressHook.next) && (index2 = index2.alternate, ReactSharedInternals.H = null === index2 || null === index2.memoizedState ? HooksDispatcherOnMount : HooksDispatcherOnUpdate); - return thenable; - } - function use2(usable) { - if (null !== usable && "object" === typeof usable) { - if ("function" === typeof usable.then) return useThenable(usable); - if (usable.$$typeof === REACT_CONTEXT_TYPE) return readContext(usable); - } - throw Error(formatProdErrorMessage(438, String(usable))); - } - function useMemoCache(size) { - var memoCache = null, updateQueue = currentlyRenderingFiber.updateQueue; - null !== updateQueue && (memoCache = updateQueue.memoCache); - if (null == memoCache) { - var current3 = currentlyRenderingFiber.alternate; - null !== current3 && (current3 = current3.updateQueue, null !== current3 && (current3 = current3.memoCache, null != current3 && (memoCache = { - data: current3.data.map(function(array2) { - return array2.slice(); - }), - index: 0 - }))); - } - null == memoCache && (memoCache = { data: [], index: 0 }); - null === updateQueue && (updateQueue = createFunctionComponentUpdateQueue(), currentlyRenderingFiber.updateQueue = updateQueue); - updateQueue.memoCache = memoCache; - updateQueue = memoCache.data[memoCache.index]; - if (void 0 === updateQueue) - for (updateQueue = memoCache.data[memoCache.index] = Array(size), current3 = 0; current3 < size; current3++) - updateQueue[current3] = REACT_MEMO_CACHE_SENTINEL; - memoCache.index++; - return updateQueue; - } - function basicStateReducer(state, action2) { - return "function" === typeof action2 ? action2(state) : action2; - } - function updateReducer(reducer) { - var hook = updateWorkInProgressHook(); - return updateReducerImpl(hook, currentHook, reducer); - } - function updateReducerImpl(hook, current3, reducer) { - var queue = hook.queue; - if (null === queue) throw Error(formatProdErrorMessage(311)); - queue.lastRenderedReducer = reducer; - var baseQueue = hook.baseQueue, pendingQueue = queue.pending; - if (null !== pendingQueue) { - if (null !== baseQueue) { - var baseFirst = baseQueue.next; - baseQueue.next = pendingQueue.next; - pendingQueue.next = baseFirst; - } - current3.baseQueue = baseQueue = pendingQueue; - queue.pending = null; - } - pendingQueue = hook.baseState; - if (null === baseQueue) hook.memoizedState = pendingQueue; - else { - current3 = baseQueue.next; - var newBaseQueueFirst = baseFirst = null, newBaseQueueLast = null, update2 = current3, didReadFromEntangledAsyncAction$60 = false; - do { - var updateLane = update2.lane & -536870913; - if (updateLane !== update2.lane ? (workInProgressRootRenderLanes & updateLane) === updateLane : (renderLanes & updateLane) === updateLane) { - var revertLane = update2.revertLane; - if (0 === revertLane) - null !== newBaseQueueLast && (newBaseQueueLast = newBaseQueueLast.next = { - lane: 0, - revertLane: 0, - gesture: null, - action: update2.action, - hasEagerState: update2.hasEagerState, - eagerState: update2.eagerState, - next: null - }), updateLane === currentEntangledLane && (didReadFromEntangledAsyncAction$60 = true); - else if ((renderLanes & revertLane) === revertLane) { - update2 = update2.next; - revertLane === currentEntangledLane && (didReadFromEntangledAsyncAction$60 = true); - continue; - } else - updateLane = { - lane: 0, - revertLane: update2.revertLane, - gesture: null, - action: update2.action, - hasEagerState: update2.hasEagerState, - eagerState: update2.eagerState, - next: null - }, null === newBaseQueueLast ? (newBaseQueueFirst = newBaseQueueLast = updateLane, baseFirst = pendingQueue) : newBaseQueueLast = newBaseQueueLast.next = updateLane, currentlyRenderingFiber.lanes |= revertLane, workInProgressRootSkippedLanes |= revertLane; - updateLane = update2.action; - shouldDoubleInvokeUserFnsInHooksDEV && reducer(pendingQueue, updateLane); - pendingQueue = update2.hasEagerState ? update2.eagerState : reducer(pendingQueue, updateLane); - } else - revertLane = { - lane: updateLane, - revertLane: update2.revertLane, - gesture: update2.gesture, - action: update2.action, - hasEagerState: update2.hasEagerState, - eagerState: update2.eagerState, - next: null - }, null === newBaseQueueLast ? (newBaseQueueFirst = newBaseQueueLast = revertLane, baseFirst = pendingQueue) : newBaseQueueLast = newBaseQueueLast.next = revertLane, currentlyRenderingFiber.lanes |= updateLane, workInProgressRootSkippedLanes |= updateLane; - update2 = update2.next; - } while (null !== update2 && update2 !== current3); - null === newBaseQueueLast ? baseFirst = pendingQueue : newBaseQueueLast.next = newBaseQueueFirst; - if (!objectIs(pendingQueue, hook.memoizedState) && (didReceiveUpdate = true, didReadFromEntangledAsyncAction$60 && (reducer = currentEntangledActionThenable, null !== reducer))) - throw reducer; - hook.memoizedState = pendingQueue; - hook.baseState = baseFirst; - hook.baseQueue = newBaseQueueLast; - queue.lastRenderedState = pendingQueue; - } - null === baseQueue && (queue.lanes = 0); - return [hook.memoizedState, queue.dispatch]; - } - function rerenderReducer(reducer) { - var hook = updateWorkInProgressHook(), queue = hook.queue; - if (null === queue) throw Error(formatProdErrorMessage(311)); - queue.lastRenderedReducer = reducer; - var dispatch = queue.dispatch, lastRenderPhaseUpdate = queue.pending, newState = hook.memoizedState; - if (null !== lastRenderPhaseUpdate) { - queue.pending = null; - var update2 = lastRenderPhaseUpdate = lastRenderPhaseUpdate.next; - do - newState = reducer(newState, update2.action), update2 = update2.next; - while (update2 !== lastRenderPhaseUpdate); - objectIs(newState, hook.memoizedState) || (didReceiveUpdate = true); - hook.memoizedState = newState; - null === hook.baseQueue && (hook.baseState = newState); - queue.lastRenderedState = newState; - } - return [newState, dispatch]; - } - function updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) { - var fiber = currentlyRenderingFiber, hook = updateWorkInProgressHook(), isHydrating$jscomp$0 = isHydrating; - if (isHydrating$jscomp$0) { - if (void 0 === getServerSnapshot) throw Error(formatProdErrorMessage(407)); - getServerSnapshot = getServerSnapshot(); - } else getServerSnapshot = getSnapshot(); - var snapshotChanged = !objectIs( - (currentHook || hook).memoizedState, - getServerSnapshot - ); - snapshotChanged && (hook.memoizedState = getServerSnapshot, didReceiveUpdate = true); - hook = hook.queue; - updateEffect(subscribeToStore.bind(null, fiber, hook, subscribe), [ - subscribe - ]); - if (hook.getSnapshot !== getSnapshot || snapshotChanged || null !== workInProgressHook && workInProgressHook.memoizedState.tag & 1) { - fiber.flags |= 2048; - pushSimpleEffect( - 9, - { destroy: void 0 }, - updateStoreInstance.bind( - null, - fiber, - hook, - getServerSnapshot, - getSnapshot - ), - null - ); - if (null === workInProgressRoot) throw Error(formatProdErrorMessage(349)); - isHydrating$jscomp$0 || 0 !== (renderLanes & 127) || pushStoreConsistencyCheck(fiber, getSnapshot, getServerSnapshot); - } - return getServerSnapshot; - } - function pushStoreConsistencyCheck(fiber, getSnapshot, renderedSnapshot) { - fiber.flags |= 16384; - fiber = { getSnapshot, value: renderedSnapshot }; - getSnapshot = currentlyRenderingFiber.updateQueue; - null === getSnapshot ? (getSnapshot = createFunctionComponentUpdateQueue(), currentlyRenderingFiber.updateQueue = getSnapshot, getSnapshot.stores = [fiber]) : (renderedSnapshot = getSnapshot.stores, null === renderedSnapshot ? getSnapshot.stores = [fiber] : renderedSnapshot.push(fiber)); - } - function updateStoreInstance(fiber, inst, nextSnapshot, getSnapshot) { - inst.value = nextSnapshot; - inst.getSnapshot = getSnapshot; - checkIfSnapshotChanged(inst) && forceStoreRerender(fiber); - } - function subscribeToStore(fiber, inst, subscribe) { - return subscribe(function() { - checkIfSnapshotChanged(inst) && forceStoreRerender(fiber); - }); - } - function checkIfSnapshotChanged(inst) { - var latestGetSnapshot = inst.getSnapshot; - inst = inst.value; - try { - var nextValue = latestGetSnapshot(); - return !objectIs(inst, nextValue); - } catch (error2) { - return true; - } - } - function forceStoreRerender(fiber) { - var root3 = enqueueConcurrentRenderForLane(fiber, 2); - null !== root3 && scheduleUpdateOnFiber(root3, fiber, 2); - } - function mountStateImpl(initialState) { - var hook = mountWorkInProgressHook(); - if ("function" === typeof initialState) { - var initialStateInitializer = initialState; - initialState = initialStateInitializer(); - if (shouldDoubleInvokeUserFnsInHooksDEV) { - setIsStrictModeForDevtools(true); - try { - initialStateInitializer(); - } finally { - setIsStrictModeForDevtools(false); - } - } - } - hook.memoizedState = hook.baseState = initialState; - hook.queue = { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: basicStateReducer, - lastRenderedState: initialState - }; - return hook; - } - function updateOptimisticImpl(hook, current3, passthrough, reducer) { - hook.baseState = passthrough; - return updateReducerImpl( - hook, - currentHook, - "function" === typeof reducer ? reducer : basicStateReducer - ); - } - function dispatchActionState(fiber, actionQueue, setPendingState, setState2, payload) { - if (isRenderPhaseUpdate(fiber)) throw Error(formatProdErrorMessage(485)); - fiber = actionQueue.action; - if (null !== fiber) { - var actionNode = { - payload, - action: fiber, - next: null, - isTransition: true, - status: "pending", - value: null, - reason: null, - listeners: [], - then: function(listener) { - actionNode.listeners.push(listener); - } - }; - null !== ReactSharedInternals.T ? setPendingState(true) : actionNode.isTransition = false; - setState2(actionNode); - setPendingState = actionQueue.pending; - null === setPendingState ? (actionNode.next = actionQueue.pending = actionNode, runActionStateAction(actionQueue, actionNode)) : (actionNode.next = setPendingState.next, actionQueue.pending = setPendingState.next = actionNode); - } - } - function runActionStateAction(actionQueue, node2) { - var action2 = node2.action, payload = node2.payload, prevState = actionQueue.state; - if (node2.isTransition) { - var prevTransition = ReactSharedInternals.T, currentTransition = {}; - ReactSharedInternals.T = currentTransition; - try { - var returnValue = action2(prevState, payload), onStartTransitionFinish = ReactSharedInternals.S; - null !== onStartTransitionFinish && onStartTransitionFinish(currentTransition, returnValue); - handleActionReturnValue(actionQueue, node2, returnValue); - } catch (error2) { - onActionError(actionQueue, node2, error2); - } finally { - null !== prevTransition && null !== currentTransition.types && (prevTransition.types = currentTransition.types), ReactSharedInternals.T = prevTransition; - } - } else - try { - prevTransition = action2(prevState, payload), handleActionReturnValue(actionQueue, node2, prevTransition); - } catch (error$66) { - onActionError(actionQueue, node2, error$66); - } - } - function handleActionReturnValue(actionQueue, node2, returnValue) { - null !== returnValue && "object" === typeof returnValue && "function" === typeof returnValue.then ? returnValue.then( - function(nextState) { - onActionSuccess(actionQueue, node2, nextState); - }, - function(error2) { - return onActionError(actionQueue, node2, error2); - } - ) : onActionSuccess(actionQueue, node2, returnValue); - } - function onActionSuccess(actionQueue, actionNode, nextState) { - actionNode.status = "fulfilled"; - actionNode.value = nextState; - notifyActionListeners(actionNode); - actionQueue.state = nextState; - actionNode = actionQueue.pending; - null !== actionNode && (nextState = actionNode.next, nextState === actionNode ? actionQueue.pending = null : (nextState = nextState.next, actionNode.next = nextState, runActionStateAction(actionQueue, nextState))); - } - function onActionError(actionQueue, actionNode, error2) { - var last2 = actionQueue.pending; - actionQueue.pending = null; - if (null !== last2) { - last2 = last2.next; - do - actionNode.status = "rejected", actionNode.reason = error2, notifyActionListeners(actionNode), actionNode = actionNode.next; - while (actionNode !== last2); - } - actionQueue.action = null; - } - function notifyActionListeners(actionNode) { - actionNode = actionNode.listeners; - for (var i4 = 0; i4 < actionNode.length; i4++) (0, actionNode[i4])(); - } - function actionStateReducer(oldState, newState) { - return newState; - } - function mountActionState(action2, initialStateProp) { - if (isHydrating) { - var ssrFormState = workInProgressRoot.formState; - if (null !== ssrFormState) { - a: { - var JSCompiler_inline_result = currentlyRenderingFiber; - if (isHydrating) { - if (nextHydratableInstance) { - b: { - var JSCompiler_inline_result$jscomp$0 = nextHydratableInstance; - for (var inRootOrSingleton = rootOrSingletonContext; 8 !== JSCompiler_inline_result$jscomp$0.nodeType; ) { - if (!inRootOrSingleton) { - JSCompiler_inline_result$jscomp$0 = null; - break b; - } - JSCompiler_inline_result$jscomp$0 = getNextHydratable( - JSCompiler_inline_result$jscomp$0.nextSibling - ); - if (null === JSCompiler_inline_result$jscomp$0) { - JSCompiler_inline_result$jscomp$0 = null; - break b; - } - } - inRootOrSingleton = JSCompiler_inline_result$jscomp$0.data; - JSCompiler_inline_result$jscomp$0 = "F!" === inRootOrSingleton || "F" === inRootOrSingleton ? JSCompiler_inline_result$jscomp$0 : null; - } - if (JSCompiler_inline_result$jscomp$0) { - nextHydratableInstance = getNextHydratable( - JSCompiler_inline_result$jscomp$0.nextSibling - ); - JSCompiler_inline_result = "F!" === JSCompiler_inline_result$jscomp$0.data; - break a; - } - } - throwOnHydrationMismatch(JSCompiler_inline_result); - } - JSCompiler_inline_result = false; - } - JSCompiler_inline_result && (initialStateProp = ssrFormState[0]); - } - } - ssrFormState = mountWorkInProgressHook(); - ssrFormState.memoizedState = ssrFormState.baseState = initialStateProp; - JSCompiler_inline_result = { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: actionStateReducer, - lastRenderedState: initialStateProp - }; - ssrFormState.queue = JSCompiler_inline_result; - ssrFormState = dispatchSetState.bind( - null, - currentlyRenderingFiber, - JSCompiler_inline_result - ); - JSCompiler_inline_result.dispatch = ssrFormState; - JSCompiler_inline_result = mountStateImpl(false); - inRootOrSingleton = dispatchOptimisticSetState.bind( - null, - currentlyRenderingFiber, - false, - JSCompiler_inline_result.queue - ); - JSCompiler_inline_result = mountWorkInProgressHook(); - JSCompiler_inline_result$jscomp$0 = { - state: initialStateProp, - dispatch: null, - action: action2, - pending: null - }; - JSCompiler_inline_result.queue = JSCompiler_inline_result$jscomp$0; - ssrFormState = dispatchActionState.bind( - null, - currentlyRenderingFiber, - JSCompiler_inline_result$jscomp$0, - inRootOrSingleton, - ssrFormState - ); - JSCompiler_inline_result$jscomp$0.dispatch = ssrFormState; - JSCompiler_inline_result.memoizedState = action2; - return [initialStateProp, ssrFormState, false]; - } - function updateActionState(action2) { - var stateHook = updateWorkInProgressHook(); - return updateActionStateImpl(stateHook, currentHook, action2); - } - function updateActionStateImpl(stateHook, currentStateHook, action2) { - currentStateHook = updateReducerImpl( - stateHook, - currentStateHook, - actionStateReducer - )[0]; - stateHook = updateReducer(basicStateReducer)[0]; - if ("object" === typeof currentStateHook && null !== currentStateHook && "function" === typeof currentStateHook.then) - try { - var state = useThenable(currentStateHook); - } catch (x2) { - if (x2 === SuspenseException) throw SuspenseActionException; - throw x2; - } - else state = currentStateHook; - currentStateHook = updateWorkInProgressHook(); - var actionQueue = currentStateHook.queue, dispatch = actionQueue.dispatch; - action2 !== currentStateHook.memoizedState && (currentlyRenderingFiber.flags |= 2048, pushSimpleEffect( - 9, - { destroy: void 0 }, - actionStateActionEffect.bind(null, actionQueue, action2), - null - )); - return [state, dispatch, stateHook]; - } - function actionStateActionEffect(actionQueue, action2) { - actionQueue.action = action2; - } - function rerenderActionState(action2) { - var stateHook = updateWorkInProgressHook(), currentStateHook = currentHook; - if (null !== currentStateHook) - return updateActionStateImpl(stateHook, currentStateHook, action2); - updateWorkInProgressHook(); - stateHook = stateHook.memoizedState; - currentStateHook = updateWorkInProgressHook(); - var dispatch = currentStateHook.queue.dispatch; - currentStateHook.memoizedState = action2; - return [stateHook, dispatch, false]; - } - function pushSimpleEffect(tag, inst, create2, deps) { - tag = { tag, create: create2, deps, inst, next: null }; - inst = currentlyRenderingFiber.updateQueue; - null === inst && (inst = createFunctionComponentUpdateQueue(), currentlyRenderingFiber.updateQueue = inst); - create2 = inst.lastEffect; - null === create2 ? inst.lastEffect = tag.next = tag : (deps = create2.next, create2.next = tag, tag.next = deps, inst.lastEffect = tag); - return tag; - } - function updateRef() { - return updateWorkInProgressHook().memoizedState; - } - function mountEffectImpl(fiberFlags, hookFlags, create2, deps) { - var hook = mountWorkInProgressHook(); - currentlyRenderingFiber.flags |= fiberFlags; - hook.memoizedState = pushSimpleEffect( - 1 | hookFlags, - { destroy: void 0 }, - create2, - void 0 === deps ? null : deps - ); - } - function updateEffectImpl(fiberFlags, hookFlags, create2, deps) { - var hook = updateWorkInProgressHook(); - deps = void 0 === deps ? null : deps; - var inst = hook.memoizedState.inst; - null !== currentHook && null !== deps && areHookInputsEqual(deps, currentHook.memoizedState.deps) ? hook.memoizedState = pushSimpleEffect(hookFlags, inst, create2, deps) : (currentlyRenderingFiber.flags |= fiberFlags, hook.memoizedState = pushSimpleEffect( - 1 | hookFlags, - inst, - create2, - deps - )); - } - function mountEffect(create2, deps) { - mountEffectImpl(8390656, 8, create2, deps); - } - function updateEffect(create2, deps) { - updateEffectImpl(2048, 8, create2, deps); - } - function useEffectEventImpl(payload) { - currentlyRenderingFiber.flags |= 4; - var componentUpdateQueue = currentlyRenderingFiber.updateQueue; - if (null === componentUpdateQueue) - componentUpdateQueue = createFunctionComponentUpdateQueue(), currentlyRenderingFiber.updateQueue = componentUpdateQueue, componentUpdateQueue.events = [payload]; - else { - var events = componentUpdateQueue.events; - null === events ? componentUpdateQueue.events = [payload] : events.push(payload); - } - } - function updateEvent(callback) { - var ref2 = updateWorkInProgressHook().memoizedState; - useEffectEventImpl({ ref: ref2, nextImpl: callback }); - return function() { - if (0 !== (executionContext & 2)) throw Error(formatProdErrorMessage(440)); - return ref2.impl.apply(void 0, arguments); - }; - } - function updateInsertionEffect(create2, deps) { - return updateEffectImpl(4, 2, create2, deps); - } - function updateLayoutEffect(create2, deps) { - return updateEffectImpl(4, 4, create2, deps); - } - function imperativeHandleEffect(create2, ref2) { - if ("function" === typeof ref2) { - create2 = create2(); - var refCleanup = ref2(create2); - return function() { - "function" === typeof refCleanup ? refCleanup() : ref2(null); - }; - } - if (null !== ref2 && void 0 !== ref2) - return create2 = create2(), ref2.current = create2, function() { - ref2.current = null; - }; - } - function updateImperativeHandle(ref2, create2, deps) { - deps = null !== deps && void 0 !== deps ? deps.concat([ref2]) : null; - updateEffectImpl(4, 4, imperativeHandleEffect.bind(null, create2, ref2), deps); - } - function mountDebugValue() { - } - function updateCallback(callback, deps) { - var hook = updateWorkInProgressHook(); - deps = void 0 === deps ? null : deps; - var prevState = hook.memoizedState; - if (null !== deps && areHookInputsEqual(deps, prevState[1])) - return prevState[0]; - hook.memoizedState = [callback, deps]; - return callback; - } - function updateMemo(nextCreate, deps) { - var hook = updateWorkInProgressHook(); - deps = void 0 === deps ? null : deps; - var prevState = hook.memoizedState; - if (null !== deps && areHookInputsEqual(deps, prevState[1])) - return prevState[0]; - prevState = nextCreate(); - if (shouldDoubleInvokeUserFnsInHooksDEV) { - setIsStrictModeForDevtools(true); - try { - nextCreate(); - } finally { - setIsStrictModeForDevtools(false); - } - } - hook.memoizedState = [prevState, deps]; - return prevState; - } - function mountDeferredValueImpl(hook, value2, initialValue) { - if (void 0 === initialValue || 0 !== (renderLanes & 1073741824) && 0 === (workInProgressRootRenderLanes & 261930)) - return hook.memoizedState = value2; - hook.memoizedState = initialValue; - hook = requestDeferredLane(); - currentlyRenderingFiber.lanes |= hook; - workInProgressRootSkippedLanes |= hook; - return initialValue; - } - function updateDeferredValueImpl(hook, prevValue, value2, initialValue) { - if (objectIs(value2, prevValue)) return value2; - if (null !== currentTreeHiddenStackCursor.current) - return hook = mountDeferredValueImpl(hook, value2, initialValue), objectIs(hook, prevValue) || (didReceiveUpdate = true), hook; - if (0 === (renderLanes & 42) || 0 !== (renderLanes & 1073741824) && 0 === (workInProgressRootRenderLanes & 261930)) - return didReceiveUpdate = true, hook.memoizedState = value2; - hook = requestDeferredLane(); - currentlyRenderingFiber.lanes |= hook; - workInProgressRootSkippedLanes |= hook; - return prevValue; - } - function startTransition2(fiber, queue, pendingState, finishedState, callback) { - var previousPriority = ReactDOMSharedInternals.p; - ReactDOMSharedInternals.p = 0 !== previousPriority && 8 > previousPriority ? previousPriority : 8; - var prevTransition = ReactSharedInternals.T, currentTransition = {}; - ReactSharedInternals.T = currentTransition; - dispatchOptimisticSetState(fiber, false, queue, pendingState); - try { - var returnValue = callback(), onStartTransitionFinish = ReactSharedInternals.S; - null !== onStartTransitionFinish && onStartTransitionFinish(currentTransition, returnValue); - if (null !== returnValue && "object" === typeof returnValue && "function" === typeof returnValue.then) { - var thenableForFinishedState = chainThenableValue( - returnValue, - finishedState - ); - dispatchSetStateInternal( - fiber, - queue, - thenableForFinishedState, - requestUpdateLane(fiber) - ); - } else - dispatchSetStateInternal( - fiber, - queue, - finishedState, - requestUpdateLane(fiber) - ); - } catch (error2) { - dispatchSetStateInternal( - fiber, - queue, - { then: function() { - }, status: "rejected", reason: error2 }, - requestUpdateLane() - ); - } finally { - ReactDOMSharedInternals.p = previousPriority, null !== prevTransition && null !== currentTransition.types && (prevTransition.types = currentTransition.types), ReactSharedInternals.T = prevTransition; - } - } - function noop2() { - } - function startHostTransition(formFiber, pendingState, action2, formData) { - if (5 !== formFiber.tag) throw Error(formatProdErrorMessage(476)); - var queue = ensureFormComponentIsStateful(formFiber).queue; - startTransition2( - formFiber, - queue, - pendingState, - sharedNotPendingObject, - null === action2 ? noop2 : function() { - requestFormReset$1(formFiber); - return action2(formData); - } - ); - } - function ensureFormComponentIsStateful(formFiber) { - var existingStateHook = formFiber.memoizedState; - if (null !== existingStateHook) return existingStateHook; - existingStateHook = { - memoizedState: sharedNotPendingObject, - baseState: sharedNotPendingObject, - baseQueue: null, - queue: { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: basicStateReducer, - lastRenderedState: sharedNotPendingObject - }, - next: null - }; - var initialResetState = {}; - existingStateHook.next = { - memoizedState: initialResetState, - baseState: initialResetState, - baseQueue: null, - queue: { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: basicStateReducer, - lastRenderedState: initialResetState - }, - next: null - }; - formFiber.memoizedState = existingStateHook; - formFiber = formFiber.alternate; - null !== formFiber && (formFiber.memoizedState = existingStateHook); - return existingStateHook; - } - function requestFormReset$1(formFiber) { - var stateHook = ensureFormComponentIsStateful(formFiber); - null === stateHook.next && (stateHook = formFiber.alternate.memoizedState); - dispatchSetStateInternal( - formFiber, - stateHook.next.queue, - {}, - requestUpdateLane() - ); - } - function useHostTransitionStatus() { - return readContext(HostTransitionContext); - } - function updateId() { - return updateWorkInProgressHook().memoizedState; - } - function updateRefresh() { - return updateWorkInProgressHook().memoizedState; - } - function refreshCache(fiber) { - for (var provider = fiber.return; null !== provider; ) { - switch (provider.tag) { - case 24: - case 3: - var lane = requestUpdateLane(); - fiber = createUpdate(lane); - var root$69 = enqueueUpdate(provider, fiber, lane); - null !== root$69 && (scheduleUpdateOnFiber(root$69, provider, lane), entangleTransitions(root$69, provider, lane)); - provider = { cache: createCache() }; - fiber.payload = provider; - return; - } - provider = provider.return; - } - } - function dispatchReducerAction(fiber, queue, action2) { - var lane = requestUpdateLane(); - action2 = { - lane, - revertLane: 0, - gesture: null, - action: action2, - hasEagerState: false, - eagerState: null, - next: null - }; - isRenderPhaseUpdate(fiber) ? enqueueRenderPhaseUpdate(queue, action2) : (action2 = enqueueConcurrentHookUpdate(fiber, queue, action2, lane), null !== action2 && (scheduleUpdateOnFiber(action2, fiber, lane), entangleTransitionUpdate(action2, queue, lane))); - } - function dispatchSetState(fiber, queue, action2) { - var lane = requestUpdateLane(); - dispatchSetStateInternal(fiber, queue, action2, lane); - } - function dispatchSetStateInternal(fiber, queue, action2, lane) { - var update2 = { - lane, - revertLane: 0, - gesture: null, - action: action2, - hasEagerState: false, - eagerState: null, - next: null - }; - if (isRenderPhaseUpdate(fiber)) enqueueRenderPhaseUpdate(queue, update2); - else { - var alternate = fiber.alternate; - if (0 === fiber.lanes && (null === alternate || 0 === alternate.lanes) && (alternate = queue.lastRenderedReducer, null !== alternate)) - try { - var currentState = queue.lastRenderedState, eagerState = alternate(currentState, action2); - update2.hasEagerState = true; - update2.eagerState = eagerState; - if (objectIs(eagerState, currentState)) - return enqueueUpdate$1(fiber, queue, update2, 0), null === workInProgressRoot && finishQueueingConcurrentUpdates(), false; - } catch (error2) { - } finally { - } - action2 = enqueueConcurrentHookUpdate(fiber, queue, update2, lane); - if (null !== action2) - return scheduleUpdateOnFiber(action2, fiber, lane), entangleTransitionUpdate(action2, queue, lane), true; - } - return false; - } - function dispatchOptimisticSetState(fiber, throwIfDuringRender, queue, action2) { - action2 = { - lane: 2, - revertLane: requestTransitionLane(), - gesture: null, - action: action2, - hasEagerState: false, - eagerState: null, - next: null - }; - if (isRenderPhaseUpdate(fiber)) { - if (throwIfDuringRender) throw Error(formatProdErrorMessage(479)); - } else - throwIfDuringRender = enqueueConcurrentHookUpdate( - fiber, - queue, - action2, - 2 - ), null !== throwIfDuringRender && scheduleUpdateOnFiber(throwIfDuringRender, fiber, 2); - } - function isRenderPhaseUpdate(fiber) { - var alternate = fiber.alternate; - return fiber === currentlyRenderingFiber || null !== alternate && alternate === currentlyRenderingFiber; - } - function enqueueRenderPhaseUpdate(queue, update2) { - didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; - var pending = queue.pending; - null === pending ? update2.next = update2 : (update2.next = pending.next, pending.next = update2); - queue.pending = update2; - } - function entangleTransitionUpdate(root3, queue, lane) { - if (0 !== (lane & 4194048)) { - var queueLanes = queue.lanes; - queueLanes &= root3.pendingLanes; - lane |= queueLanes; - queue.lanes = lane; - markRootEntangled(root3, lane); - } - } - var ContextOnlyDispatcher = { - readContext, - use: use2, - useCallback: throwInvalidHookError, - useContext: throwInvalidHookError, - useEffect: throwInvalidHookError, - useImperativeHandle: throwInvalidHookError, - useLayoutEffect: throwInvalidHookError, - useInsertionEffect: throwInvalidHookError, - useMemo: throwInvalidHookError, - useReducer: throwInvalidHookError, - useRef: throwInvalidHookError, - useState: throwInvalidHookError, - useDebugValue: throwInvalidHookError, - useDeferredValue: throwInvalidHookError, - useTransition: throwInvalidHookError, - useSyncExternalStore: throwInvalidHookError, - useId: throwInvalidHookError, - useHostTransitionStatus: throwInvalidHookError, - useFormState: throwInvalidHookError, - useActionState: throwInvalidHookError, - useOptimistic: throwInvalidHookError, - useMemoCache: throwInvalidHookError, - useCacheRefresh: throwInvalidHookError - }; - ContextOnlyDispatcher.useEffectEvent = throwInvalidHookError; - var HooksDispatcherOnMount = { - readContext, - use: use2, - useCallback: function(callback, deps) { - mountWorkInProgressHook().memoizedState = [ - callback, - void 0 === deps ? null : deps - ]; - return callback; - }, - useContext: readContext, - useEffect: mountEffect, - useImperativeHandle: function(ref2, create2, deps) { - deps = null !== deps && void 0 !== deps ? deps.concat([ref2]) : null; - mountEffectImpl( - 4194308, - 4, - imperativeHandleEffect.bind(null, create2, ref2), - deps - ); - }, - useLayoutEffect: function(create2, deps) { - return mountEffectImpl(4194308, 4, create2, deps); - }, - useInsertionEffect: function(create2, deps) { - mountEffectImpl(4, 2, create2, deps); - }, - useMemo: function(nextCreate, deps) { - var hook = mountWorkInProgressHook(); - deps = void 0 === deps ? null : deps; - var nextValue = nextCreate(); - if (shouldDoubleInvokeUserFnsInHooksDEV) { - setIsStrictModeForDevtools(true); - try { - nextCreate(); - } finally { - setIsStrictModeForDevtools(false); - } - } - hook.memoizedState = [nextValue, deps]; - return nextValue; - }, - useReducer: function(reducer, initialArg, init) { - var hook = mountWorkInProgressHook(); - if (void 0 !== init) { - var initialState = init(initialArg); - if (shouldDoubleInvokeUserFnsInHooksDEV) { - setIsStrictModeForDevtools(true); - try { - init(initialArg); - } finally { - setIsStrictModeForDevtools(false); - } - } - } else initialState = initialArg; - hook.memoizedState = hook.baseState = initialState; - reducer = { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: reducer, - lastRenderedState: initialState - }; - hook.queue = reducer; - reducer = reducer.dispatch = dispatchReducerAction.bind( - null, - currentlyRenderingFiber, - reducer - ); - return [hook.memoizedState, reducer]; - }, - useRef: function(initialValue) { - var hook = mountWorkInProgressHook(); - initialValue = { current: initialValue }; - return hook.memoizedState = initialValue; - }, - useState: function(initialState) { - initialState = mountStateImpl(initialState); - var queue = initialState.queue, dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue); - queue.dispatch = dispatch; - return [initialState.memoizedState, dispatch]; - }, - useDebugValue: mountDebugValue, - useDeferredValue: function(value2, initialValue) { - var hook = mountWorkInProgressHook(); - return mountDeferredValueImpl(hook, value2, initialValue); - }, - useTransition: function() { - var stateHook = mountStateImpl(false); - stateHook = startTransition2.bind( - null, - currentlyRenderingFiber, - stateHook.queue, - true, - false - ); - mountWorkInProgressHook().memoizedState = stateHook; - return [false, stateHook]; - }, - useSyncExternalStore: function(subscribe, getSnapshot, getServerSnapshot) { - var fiber = currentlyRenderingFiber, hook = mountWorkInProgressHook(); - if (isHydrating) { - if (void 0 === getServerSnapshot) - throw Error(formatProdErrorMessage(407)); - getServerSnapshot = getServerSnapshot(); - } else { - getServerSnapshot = getSnapshot(); - if (null === workInProgressRoot) - throw Error(formatProdErrorMessage(349)); - 0 !== (workInProgressRootRenderLanes & 127) || pushStoreConsistencyCheck(fiber, getSnapshot, getServerSnapshot); - } - hook.memoizedState = getServerSnapshot; - var inst = { value: getServerSnapshot, getSnapshot }; - hook.queue = inst; - mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [ - subscribe - ]); - fiber.flags |= 2048; - pushSimpleEffect( - 9, - { destroy: void 0 }, - updateStoreInstance.bind( - null, - fiber, - inst, - getServerSnapshot, - getSnapshot - ), - null - ); - return getServerSnapshot; - }, - useId: function() { - var hook = mountWorkInProgressHook(), identifierPrefix = workInProgressRoot.identifierPrefix; - if (isHydrating) { - var JSCompiler_inline_result = treeContextOverflow; - var idWithLeadingBit = treeContextId; - JSCompiler_inline_result = (idWithLeadingBit & ~(1 << 32 - clz322(idWithLeadingBit) - 1)).toString(32) + JSCompiler_inline_result; - identifierPrefix = "_" + identifierPrefix + "R_" + JSCompiler_inline_result; - JSCompiler_inline_result = localIdCounter++; - 0 < JSCompiler_inline_result && (identifierPrefix += "H" + JSCompiler_inline_result.toString(32)); - identifierPrefix += "_"; - } else - JSCompiler_inline_result = globalClientIdCounter++, identifierPrefix = "_" + identifierPrefix + "r_" + JSCompiler_inline_result.toString(32) + "_"; - return hook.memoizedState = identifierPrefix; - }, - useHostTransitionStatus, - useFormState: mountActionState, - useActionState: mountActionState, - useOptimistic: function(passthrough) { - var hook = mountWorkInProgressHook(); - hook.memoizedState = hook.baseState = passthrough; - var queue = { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: null, - lastRenderedState: null - }; - hook.queue = queue; - hook = dispatchOptimisticSetState.bind( - null, - currentlyRenderingFiber, - true, - queue - ); - queue.dispatch = hook; - return [passthrough, hook]; - }, - useMemoCache, - useCacheRefresh: function() { - return mountWorkInProgressHook().memoizedState = refreshCache.bind( - null, - currentlyRenderingFiber - ); - }, - useEffectEvent: function(callback) { - var hook = mountWorkInProgressHook(), ref2 = { impl: callback }; - hook.memoizedState = ref2; - return function() { - if (0 !== (executionContext & 2)) - throw Error(formatProdErrorMessage(440)); - return ref2.impl.apply(void 0, arguments); - }; - } - }, HooksDispatcherOnUpdate = { - readContext, - use: use2, - useCallback: updateCallback, - useContext: readContext, - useEffect: updateEffect, - useImperativeHandle: updateImperativeHandle, - useInsertionEffect: updateInsertionEffect, - useLayoutEffect: updateLayoutEffect, - useMemo: updateMemo, - useReducer: updateReducer, - useRef: updateRef, - useState: function() { - return updateReducer(basicStateReducer); - }, - useDebugValue: mountDebugValue, - useDeferredValue: function(value2, initialValue) { - var hook = updateWorkInProgressHook(); - return updateDeferredValueImpl( - hook, - currentHook.memoizedState, - value2, - initialValue - ); - }, - useTransition: function() { - var booleanOrThenable = updateReducer(basicStateReducer)[0], start2 = updateWorkInProgressHook().memoizedState; - return [ - "boolean" === typeof booleanOrThenable ? booleanOrThenable : useThenable(booleanOrThenable), - start2 - ]; - }, - useSyncExternalStore: updateSyncExternalStore, - useId: updateId, - useHostTransitionStatus, - useFormState: updateActionState, - useActionState: updateActionState, - useOptimistic: function(passthrough, reducer) { - var hook = updateWorkInProgressHook(); - return updateOptimisticImpl(hook, currentHook, passthrough, reducer); - }, - useMemoCache, - useCacheRefresh: updateRefresh - }; - HooksDispatcherOnUpdate.useEffectEvent = updateEvent; - var HooksDispatcherOnRerender = { - readContext, - use: use2, - useCallback: updateCallback, - useContext: readContext, - useEffect: updateEffect, - useImperativeHandle: updateImperativeHandle, - useInsertionEffect: updateInsertionEffect, - useLayoutEffect: updateLayoutEffect, - useMemo: updateMemo, - useReducer: rerenderReducer, - useRef: updateRef, - useState: function() { - return rerenderReducer(basicStateReducer); - }, - useDebugValue: mountDebugValue, - useDeferredValue: function(value2, initialValue) { - var hook = updateWorkInProgressHook(); - return null === currentHook ? mountDeferredValueImpl(hook, value2, initialValue) : updateDeferredValueImpl( - hook, - currentHook.memoizedState, - value2, - initialValue - ); - }, - useTransition: function() { - var booleanOrThenable = rerenderReducer(basicStateReducer)[0], start2 = updateWorkInProgressHook().memoizedState; - return [ - "boolean" === typeof booleanOrThenable ? booleanOrThenable : useThenable(booleanOrThenable), - start2 - ]; - }, - useSyncExternalStore: updateSyncExternalStore, - useId: updateId, - useHostTransitionStatus, - useFormState: rerenderActionState, - useActionState: rerenderActionState, - useOptimistic: function(passthrough, reducer) { - var hook = updateWorkInProgressHook(); - if (null !== currentHook) - return updateOptimisticImpl(hook, currentHook, passthrough, reducer); - hook.baseState = passthrough; - return [passthrough, hook.queue.dispatch]; - }, - useMemoCache, - useCacheRefresh: updateRefresh - }; - HooksDispatcherOnRerender.useEffectEvent = updateEvent; - function applyDerivedStateFromProps(workInProgress2, ctor, getDerivedStateFromProps, nextProps) { - ctor = workInProgress2.memoizedState; - getDerivedStateFromProps = getDerivedStateFromProps(nextProps, ctor); - getDerivedStateFromProps = null === getDerivedStateFromProps || void 0 === getDerivedStateFromProps ? ctor : assign2({}, ctor, getDerivedStateFromProps); - workInProgress2.memoizedState = getDerivedStateFromProps; - 0 === workInProgress2.lanes && (workInProgress2.updateQueue.baseState = getDerivedStateFromProps); - } - var classComponentUpdater = { - enqueueSetState: function(inst, payload, callback) { - inst = inst._reactInternals; - var lane = requestUpdateLane(), update2 = createUpdate(lane); - update2.payload = payload; - void 0 !== callback && null !== callback && (update2.callback = callback); - payload = enqueueUpdate(inst, update2, lane); - null !== payload && (scheduleUpdateOnFiber(payload, inst, lane), entangleTransitions(payload, inst, lane)); - }, - enqueueReplaceState: function(inst, payload, callback) { - inst = inst._reactInternals; - var lane = requestUpdateLane(), update2 = createUpdate(lane); - update2.tag = 1; - update2.payload = payload; - void 0 !== callback && null !== callback && (update2.callback = callback); - payload = enqueueUpdate(inst, update2, lane); - null !== payload && (scheduleUpdateOnFiber(payload, inst, lane), entangleTransitions(payload, inst, lane)); - }, - enqueueForceUpdate: function(inst, callback) { - inst = inst._reactInternals; - var lane = requestUpdateLane(), update2 = createUpdate(lane); - update2.tag = 2; - void 0 !== callback && null !== callback && (update2.callback = callback); - callback = enqueueUpdate(inst, update2, lane); - null !== callback && (scheduleUpdateOnFiber(callback, inst, lane), entangleTransitions(callback, inst, lane)); - } - }; - function checkShouldComponentUpdate(workInProgress2, ctor, oldProps, newProps, oldState, newState, nextContext) { - workInProgress2 = workInProgress2.stateNode; - return "function" === typeof workInProgress2.shouldComponentUpdate ? workInProgress2.shouldComponentUpdate(newProps, newState, nextContext) : ctor.prototype && ctor.prototype.isPureReactComponent ? !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) : true; - } - function callComponentWillReceiveProps(workInProgress2, instance2, newProps, nextContext) { - workInProgress2 = instance2.state; - "function" === typeof instance2.componentWillReceiveProps && instance2.componentWillReceiveProps(newProps, nextContext); - "function" === typeof instance2.UNSAFE_componentWillReceiveProps && instance2.UNSAFE_componentWillReceiveProps(newProps, nextContext); - instance2.state !== workInProgress2 && classComponentUpdater.enqueueReplaceState(instance2, instance2.state, null); - } - function resolveClassComponentProps(Component2, baseProps) { - var newProps = baseProps; - if ("ref" in baseProps) { - newProps = {}; - for (var propName in baseProps) - "ref" !== propName && (newProps[propName] = baseProps[propName]); - } - if (Component2 = Component2.defaultProps) { - newProps === baseProps && (newProps = assign2({}, newProps)); - for (var propName$73 in Component2) - void 0 === newProps[propName$73] && (newProps[propName$73] = Component2[propName$73]); - } - return newProps; - } - function defaultOnUncaughtError(error2) { - reportGlobalError(error2); - } - function defaultOnCaughtError(error2) { - console.error(error2); - } - function defaultOnRecoverableError(error2) { - reportGlobalError(error2); - } - function logUncaughtError(root3, errorInfo) { - try { - var onUncaughtError = root3.onUncaughtError; - onUncaughtError(errorInfo.value, { componentStack: errorInfo.stack }); - } catch (e$74) { - setTimeout(function() { - throw e$74; - }); - } - } - function logCaughtError(root3, boundary, errorInfo) { - try { - var onCaughtError = root3.onCaughtError; - onCaughtError(errorInfo.value, { - componentStack: errorInfo.stack, - errorBoundary: 1 === boundary.tag ? boundary.stateNode : null - }); - } catch (e$75) { - setTimeout(function() { - throw e$75; - }); - } - } - function createRootErrorUpdate(root3, errorInfo, lane) { - lane = createUpdate(lane); - lane.tag = 3; - lane.payload = { element: null }; - lane.callback = function() { - logUncaughtError(root3, errorInfo); - }; - return lane; - } - function createClassErrorUpdate(lane) { - lane = createUpdate(lane); - lane.tag = 3; - return lane; - } - function initializeClassErrorUpdate(update2, root3, fiber, errorInfo) { - var getDerivedStateFromError = fiber.type.getDerivedStateFromError; - if ("function" === typeof getDerivedStateFromError) { - var error2 = errorInfo.value; - update2.payload = function() { - return getDerivedStateFromError(error2); - }; - update2.callback = function() { - logCaughtError(root3, fiber, errorInfo); - }; - } - var inst = fiber.stateNode; - null !== inst && "function" === typeof inst.componentDidCatch && (update2.callback = function() { - logCaughtError(root3, fiber, errorInfo); - "function" !== typeof getDerivedStateFromError && (null === legacyErrorBoundariesThatAlreadyFailed ? legacyErrorBoundariesThatAlreadyFailed = /* @__PURE__ */ new Set([this]) : legacyErrorBoundariesThatAlreadyFailed.add(this)); - var stack2 = errorInfo.stack; - this.componentDidCatch(errorInfo.value, { - componentStack: null !== stack2 ? stack2 : "" - }); - }); - } - function throwException(root3, returnFiber, sourceFiber, value2, rootRenderLanes) { - sourceFiber.flags |= 32768; - if (null !== value2 && "object" === typeof value2 && "function" === typeof value2.then) { - returnFiber = sourceFiber.alternate; - null !== returnFiber && propagateParentContextChanges( - returnFiber, - sourceFiber, - rootRenderLanes, - true - ); - sourceFiber = suspenseHandlerStackCursor.current; - if (null !== sourceFiber) { - switch (sourceFiber.tag) { - case 31: - case 13: - return null === shellBoundary ? renderDidSuspendDelayIfPossible() : null === sourceFiber.alternate && 0 === workInProgressRootExitStatus && (workInProgressRootExitStatus = 3), sourceFiber.flags &= -257, sourceFiber.flags |= 65536, sourceFiber.lanes = rootRenderLanes, value2 === noopSuspenseyCommitThenable ? sourceFiber.flags |= 16384 : (returnFiber = sourceFiber.updateQueue, null === returnFiber ? sourceFiber.updateQueue = /* @__PURE__ */ new Set([value2]) : returnFiber.add(value2), attachPingListener(root3, value2, rootRenderLanes)), false; - case 22: - return sourceFiber.flags |= 65536, value2 === noopSuspenseyCommitThenable ? sourceFiber.flags |= 16384 : (returnFiber = sourceFiber.updateQueue, null === returnFiber ? (returnFiber = { - transitions: null, - markerInstances: null, - retryQueue: /* @__PURE__ */ new Set([value2]) - }, sourceFiber.updateQueue = returnFiber) : (sourceFiber = returnFiber.retryQueue, null === sourceFiber ? returnFiber.retryQueue = /* @__PURE__ */ new Set([value2]) : sourceFiber.add(value2)), attachPingListener(root3, value2, rootRenderLanes)), false; - } - throw Error(formatProdErrorMessage(435, sourceFiber.tag)); - } - attachPingListener(root3, value2, rootRenderLanes); - renderDidSuspendDelayIfPossible(); - return false; - } - if (isHydrating) - return returnFiber = suspenseHandlerStackCursor.current, null !== returnFiber ? (0 === (returnFiber.flags & 65536) && (returnFiber.flags |= 256), returnFiber.flags |= 65536, returnFiber.lanes = rootRenderLanes, value2 !== HydrationMismatchException && (root3 = Error(formatProdErrorMessage(422), { cause: value2 }), queueHydrationError(createCapturedValueAtFiber(root3, sourceFiber)))) : (value2 !== HydrationMismatchException && (returnFiber = Error(formatProdErrorMessage(423), { - cause: value2 - }), queueHydrationError( - createCapturedValueAtFiber(returnFiber, sourceFiber) - )), root3 = root3.current.alternate, root3.flags |= 65536, rootRenderLanes &= -rootRenderLanes, root3.lanes |= rootRenderLanes, value2 = createCapturedValueAtFiber(value2, sourceFiber), rootRenderLanes = createRootErrorUpdate( - root3.stateNode, - value2, - rootRenderLanes - ), enqueueCapturedUpdate(root3, rootRenderLanes), 4 !== workInProgressRootExitStatus && (workInProgressRootExitStatus = 2)), false; - var wrapperError = Error(formatProdErrorMessage(520), { cause: value2 }); - wrapperError = createCapturedValueAtFiber(wrapperError, sourceFiber); - null === workInProgressRootConcurrentErrors ? workInProgressRootConcurrentErrors = [wrapperError] : workInProgressRootConcurrentErrors.push(wrapperError); - 4 !== workInProgressRootExitStatus && (workInProgressRootExitStatus = 2); - if (null === returnFiber) return true; - value2 = createCapturedValueAtFiber(value2, sourceFiber); - sourceFiber = returnFiber; - do { - switch (sourceFiber.tag) { - case 3: - return sourceFiber.flags |= 65536, root3 = rootRenderLanes & -rootRenderLanes, sourceFiber.lanes |= root3, root3 = createRootErrorUpdate(sourceFiber.stateNode, value2, root3), enqueueCapturedUpdate(sourceFiber, root3), false; - case 1: - if (returnFiber = sourceFiber.type, wrapperError = sourceFiber.stateNode, 0 === (sourceFiber.flags & 128) && ("function" === typeof returnFiber.getDerivedStateFromError || null !== wrapperError && "function" === typeof wrapperError.componentDidCatch && (null === legacyErrorBoundariesThatAlreadyFailed || !legacyErrorBoundariesThatAlreadyFailed.has(wrapperError)))) - return sourceFiber.flags |= 65536, rootRenderLanes &= -rootRenderLanes, sourceFiber.lanes |= rootRenderLanes, rootRenderLanes = createClassErrorUpdate(rootRenderLanes), initializeClassErrorUpdate( - rootRenderLanes, - root3, - sourceFiber, - value2 - ), enqueueCapturedUpdate(sourceFiber, rootRenderLanes), false; - } - sourceFiber = sourceFiber.return; - } while (null !== sourceFiber); - return false; - } - var SelectiveHydrationException = Error(formatProdErrorMessage(461)), didReceiveUpdate = false; - function reconcileChildren(current3, workInProgress2, nextChildren, renderLanes2) { - workInProgress2.child = null === current3 ? mountChildFibers(workInProgress2, null, nextChildren, renderLanes2) : reconcileChildFibers( - workInProgress2, - current3.child, - nextChildren, - renderLanes2 - ); - } - function updateForwardRef(current3, workInProgress2, Component2, nextProps, renderLanes2) { - Component2 = Component2.render; - var ref2 = workInProgress2.ref; - if ("ref" in nextProps) { - var propsWithoutRef = {}; - for (var key2 in nextProps) - "ref" !== key2 && (propsWithoutRef[key2] = nextProps[key2]); - } else propsWithoutRef = nextProps; - prepareToReadContext(workInProgress2); - nextProps = renderWithHooks( - current3, - workInProgress2, - Component2, - propsWithoutRef, - ref2, - renderLanes2 - ); - key2 = checkDidRenderIdHook(); - if (null !== current3 && !didReceiveUpdate) - return bailoutHooks(current3, workInProgress2, renderLanes2), bailoutOnAlreadyFinishedWork(current3, workInProgress2, renderLanes2); - isHydrating && key2 && pushMaterializedTreeId(workInProgress2); - workInProgress2.flags |= 1; - reconcileChildren(current3, workInProgress2, nextProps, renderLanes2); - return workInProgress2.child; - } - function updateMemoComponent(current3, workInProgress2, Component2, nextProps, renderLanes2) { - if (null === current3) { - var type = Component2.type; - if ("function" === typeof type && !shouldConstruct(type) && void 0 === type.defaultProps && null === Component2.compare) - return workInProgress2.tag = 15, workInProgress2.type = type, updateSimpleMemoComponent( - current3, - workInProgress2, - type, - nextProps, - renderLanes2 - ); - current3 = createFiberFromTypeAndProps( - Component2.type, - null, - nextProps, - workInProgress2, - workInProgress2.mode, - renderLanes2 - ); - current3.ref = workInProgress2.ref; - current3.return = workInProgress2; - return workInProgress2.child = current3; - } - type = current3.child; - if (!checkScheduledUpdateOrContext(current3, renderLanes2)) { - var prevProps = type.memoizedProps; - Component2 = Component2.compare; - Component2 = null !== Component2 ? Component2 : shallowEqual; - if (Component2(prevProps, nextProps) && current3.ref === workInProgress2.ref) - return bailoutOnAlreadyFinishedWork(current3, workInProgress2, renderLanes2); - } - workInProgress2.flags |= 1; - current3 = createWorkInProgress(type, nextProps); - current3.ref = workInProgress2.ref; - current3.return = workInProgress2; - return workInProgress2.child = current3; - } - function updateSimpleMemoComponent(current3, workInProgress2, Component2, nextProps, renderLanes2) { - if (null !== current3) { - var prevProps = current3.memoizedProps; - if (shallowEqual(prevProps, nextProps) && current3.ref === workInProgress2.ref) - if (didReceiveUpdate = false, workInProgress2.pendingProps = nextProps = prevProps, checkScheduledUpdateOrContext(current3, renderLanes2)) - 0 !== (current3.flags & 131072) && (didReceiveUpdate = true); - else - return workInProgress2.lanes = current3.lanes, bailoutOnAlreadyFinishedWork(current3, workInProgress2, renderLanes2); - } - return updateFunctionComponent( - current3, - workInProgress2, - Component2, - nextProps, - renderLanes2 - ); - } - function updateOffscreenComponent(current3, workInProgress2, renderLanes2, nextProps) { - var nextChildren = nextProps.children, prevState = null !== current3 ? current3.memoizedState : null; - null === current3 && null === workInProgress2.stateNode && (workInProgress2.stateNode = { - _visibility: 1, - _pendingMarkers: null, - _retryCache: null, - _transitions: null - }); - if ("hidden" === nextProps.mode) { - if (0 !== (workInProgress2.flags & 128)) { - prevState = null !== prevState ? prevState.baseLanes | renderLanes2 : renderLanes2; - if (null !== current3) { - nextProps = workInProgress2.child = current3.child; - for (nextChildren = 0; null !== nextProps; ) - nextChildren = nextChildren | nextProps.lanes | nextProps.childLanes, nextProps = nextProps.sibling; - nextProps = nextChildren & ~prevState; - } else nextProps = 0, workInProgress2.child = null; - return deferHiddenOffscreenComponent( - current3, - workInProgress2, - prevState, - renderLanes2, - nextProps - ); - } - if (0 !== (renderLanes2 & 536870912)) - workInProgress2.memoizedState = { baseLanes: 0, cachePool: null }, null !== current3 && pushTransition( - workInProgress2, - null !== prevState ? prevState.cachePool : null - ), null !== prevState ? pushHiddenContext(workInProgress2, prevState) : reuseHiddenContextOnStack(), pushOffscreenSuspenseHandler(workInProgress2); - else - return nextProps = workInProgress2.lanes = 536870912, deferHiddenOffscreenComponent( - current3, - workInProgress2, - null !== prevState ? prevState.baseLanes | renderLanes2 : renderLanes2, - renderLanes2, - nextProps - ); - } else - null !== prevState ? (pushTransition(workInProgress2, prevState.cachePool), pushHiddenContext(workInProgress2, prevState), reuseSuspenseHandlerOnStack(), workInProgress2.memoizedState = null) : (null !== current3 && pushTransition(workInProgress2, null), reuseHiddenContextOnStack(), reuseSuspenseHandlerOnStack()); - reconcileChildren(current3, workInProgress2, nextChildren, renderLanes2); - return workInProgress2.child; - } - function bailoutOffscreenComponent(current3, workInProgress2) { - null !== current3 && 22 === current3.tag || null !== workInProgress2.stateNode || (workInProgress2.stateNode = { - _visibility: 1, - _pendingMarkers: null, - _retryCache: null, - _transitions: null - }); - return workInProgress2.sibling; - } - function deferHiddenOffscreenComponent(current3, workInProgress2, nextBaseLanes, renderLanes2, remainingChildLanes) { - var JSCompiler_inline_result = peekCacheFromPool(); - JSCompiler_inline_result = null === JSCompiler_inline_result ? null : { parent: CacheContext._currentValue, pool: JSCompiler_inline_result }; - workInProgress2.memoizedState = { - baseLanes: nextBaseLanes, - cachePool: JSCompiler_inline_result - }; - null !== current3 && pushTransition(workInProgress2, null); - reuseHiddenContextOnStack(); - pushOffscreenSuspenseHandler(workInProgress2); - null !== current3 && propagateParentContextChanges(current3, workInProgress2, renderLanes2, true); - workInProgress2.childLanes = remainingChildLanes; - return null; - } - function mountActivityChildren(workInProgress2, nextProps) { - nextProps = mountWorkInProgressOffscreenFiber( - { mode: nextProps.mode, children: nextProps.children }, - workInProgress2.mode - ); - nextProps.ref = workInProgress2.ref; - workInProgress2.child = nextProps; - nextProps.return = workInProgress2; - return nextProps; - } - function retryActivityComponentWithoutHydrating(current3, workInProgress2, renderLanes2) { - reconcileChildFibers(workInProgress2, current3.child, null, renderLanes2); - current3 = mountActivityChildren(workInProgress2, workInProgress2.pendingProps); - current3.flags |= 2; - popSuspenseHandler(workInProgress2); - workInProgress2.memoizedState = null; - return current3; - } - function updateActivityComponent(current3, workInProgress2, renderLanes2) { - var nextProps = workInProgress2.pendingProps, didSuspend = 0 !== (workInProgress2.flags & 128); - workInProgress2.flags &= -129; - if (null === current3) { - if (isHydrating) { - if ("hidden" === nextProps.mode) - return current3 = mountActivityChildren(workInProgress2, nextProps), workInProgress2.lanes = 536870912, bailoutOffscreenComponent(null, current3); - pushDehydratedActivitySuspenseHandler(workInProgress2); - (current3 = nextHydratableInstance) ? (current3 = canHydrateHydrationBoundary( - current3, - rootOrSingletonContext - ), current3 = null !== current3 && "&" === current3.data ? current3 : null, null !== current3 && (workInProgress2.memoizedState = { - dehydrated: current3, - treeContext: null !== treeContextProvider ? { id: treeContextId, overflow: treeContextOverflow } : null, - retryLane: 536870912, - hydrationErrors: null - }, renderLanes2 = createFiberFromDehydratedFragment(current3), renderLanes2.return = workInProgress2, workInProgress2.child = renderLanes2, hydrationParentFiber = workInProgress2, nextHydratableInstance = null)) : current3 = null; - if (null === current3) throw throwOnHydrationMismatch(workInProgress2); - workInProgress2.lanes = 536870912; - return null; - } - return mountActivityChildren(workInProgress2, nextProps); - } - var prevState = current3.memoizedState; - if (null !== prevState) { - var dehydrated = prevState.dehydrated; - pushDehydratedActivitySuspenseHandler(workInProgress2); - if (didSuspend) - if (workInProgress2.flags & 256) - workInProgress2.flags &= -257, workInProgress2 = retryActivityComponentWithoutHydrating( - current3, - workInProgress2, - renderLanes2 - ); - else if (null !== workInProgress2.memoizedState) - workInProgress2.child = current3.child, workInProgress2.flags |= 128, workInProgress2 = null; - else throw Error(formatProdErrorMessage(558)); - else if (didReceiveUpdate || propagateParentContextChanges(current3, workInProgress2, renderLanes2, false), didSuspend = 0 !== (renderLanes2 & current3.childLanes), didReceiveUpdate || didSuspend) { - nextProps = workInProgressRoot; - if (null !== nextProps && (dehydrated = getBumpedLaneForHydration(nextProps, renderLanes2), 0 !== dehydrated && dehydrated !== prevState.retryLane)) - throw prevState.retryLane = dehydrated, enqueueConcurrentRenderForLane(current3, dehydrated), scheduleUpdateOnFiber(nextProps, current3, dehydrated), SelectiveHydrationException; - renderDidSuspendDelayIfPossible(); - workInProgress2 = retryActivityComponentWithoutHydrating( - current3, - workInProgress2, - renderLanes2 - ); - } else - current3 = prevState.treeContext, nextHydratableInstance = getNextHydratable(dehydrated.nextSibling), hydrationParentFiber = workInProgress2, isHydrating = true, hydrationErrors = null, rootOrSingletonContext = false, null !== current3 && restoreSuspendedTreeContext(workInProgress2, current3), workInProgress2 = mountActivityChildren(workInProgress2, nextProps), workInProgress2.flags |= 4096; - return workInProgress2; - } - current3 = createWorkInProgress(current3.child, { - mode: nextProps.mode, - children: nextProps.children - }); - current3.ref = workInProgress2.ref; - workInProgress2.child = current3; - current3.return = workInProgress2; - return current3; - } - function markRef(current3, workInProgress2) { - var ref2 = workInProgress2.ref; - if (null === ref2) - null !== current3 && null !== current3.ref && (workInProgress2.flags |= 4194816); - else { - if ("function" !== typeof ref2 && "object" !== typeof ref2) - throw Error(formatProdErrorMessage(284)); - if (null === current3 || current3.ref !== ref2) - workInProgress2.flags |= 4194816; - } - } - function updateFunctionComponent(current3, workInProgress2, Component2, nextProps, renderLanes2) { - prepareToReadContext(workInProgress2); - Component2 = renderWithHooks( - current3, - workInProgress2, - Component2, - nextProps, - void 0, - renderLanes2 - ); - nextProps = checkDidRenderIdHook(); - if (null !== current3 && !didReceiveUpdate) - return bailoutHooks(current3, workInProgress2, renderLanes2), bailoutOnAlreadyFinishedWork(current3, workInProgress2, renderLanes2); - isHydrating && nextProps && pushMaterializedTreeId(workInProgress2); - workInProgress2.flags |= 1; - reconcileChildren(current3, workInProgress2, Component2, renderLanes2); - return workInProgress2.child; - } - function replayFunctionComponent(current3, workInProgress2, nextProps, Component2, secondArg, renderLanes2) { - prepareToReadContext(workInProgress2); - workInProgress2.updateQueue = null; - nextProps = renderWithHooksAgain( - workInProgress2, - Component2, - nextProps, - secondArg - ); - finishRenderingHooks(current3); - Component2 = checkDidRenderIdHook(); - if (null !== current3 && !didReceiveUpdate) - return bailoutHooks(current3, workInProgress2, renderLanes2), bailoutOnAlreadyFinishedWork(current3, workInProgress2, renderLanes2); - isHydrating && Component2 && pushMaterializedTreeId(workInProgress2); - workInProgress2.flags |= 1; - reconcileChildren(current3, workInProgress2, nextProps, renderLanes2); - return workInProgress2.child; - } - function updateClassComponent(current3, workInProgress2, Component2, nextProps, renderLanes2) { - prepareToReadContext(workInProgress2); - if (null === workInProgress2.stateNode) { - var context = emptyContextObject, contextType = Component2.contextType; - "object" === typeof contextType && null !== contextType && (context = readContext(contextType)); - context = new Component2(nextProps, context); - workInProgress2.memoizedState = null !== context.state && void 0 !== context.state ? context.state : null; - context.updater = classComponentUpdater; - workInProgress2.stateNode = context; - context._reactInternals = workInProgress2; - context = workInProgress2.stateNode; - context.props = nextProps; - context.state = workInProgress2.memoizedState; - context.refs = {}; - initializeUpdateQueue(workInProgress2); - contextType = Component2.contextType; - context.context = "object" === typeof contextType && null !== contextType ? readContext(contextType) : emptyContextObject; - context.state = workInProgress2.memoizedState; - contextType = Component2.getDerivedStateFromProps; - "function" === typeof contextType && (applyDerivedStateFromProps( - workInProgress2, - Component2, - contextType, - nextProps - ), context.state = workInProgress2.memoizedState); - "function" === typeof Component2.getDerivedStateFromProps || "function" === typeof context.getSnapshotBeforeUpdate || "function" !== typeof context.UNSAFE_componentWillMount && "function" !== typeof context.componentWillMount || (contextType = context.state, "function" === typeof context.componentWillMount && context.componentWillMount(), "function" === typeof context.UNSAFE_componentWillMount && context.UNSAFE_componentWillMount(), contextType !== context.state && classComponentUpdater.enqueueReplaceState(context, context.state, null), processUpdateQueue(workInProgress2, nextProps, context, renderLanes2), suspendIfUpdateReadFromEntangledAsyncAction(), context.state = workInProgress2.memoizedState); - "function" === typeof context.componentDidMount && (workInProgress2.flags |= 4194308); - nextProps = true; - } else if (null === current3) { - context = workInProgress2.stateNode; - var unresolvedOldProps = workInProgress2.memoizedProps, oldProps = resolveClassComponentProps(Component2, unresolvedOldProps); - context.props = oldProps; - var oldContext = context.context, contextType$jscomp$0 = Component2.contextType; - contextType = emptyContextObject; - "object" === typeof contextType$jscomp$0 && null !== contextType$jscomp$0 && (contextType = readContext(contextType$jscomp$0)); - var getDerivedStateFromProps = Component2.getDerivedStateFromProps; - contextType$jscomp$0 = "function" === typeof getDerivedStateFromProps || "function" === typeof context.getSnapshotBeforeUpdate; - unresolvedOldProps = workInProgress2.pendingProps !== unresolvedOldProps; - contextType$jscomp$0 || "function" !== typeof context.UNSAFE_componentWillReceiveProps && "function" !== typeof context.componentWillReceiveProps || (unresolvedOldProps || oldContext !== contextType) && callComponentWillReceiveProps( - workInProgress2, - context, - nextProps, - contextType - ); - hasForceUpdate = false; - var oldState = workInProgress2.memoizedState; - context.state = oldState; - processUpdateQueue(workInProgress2, nextProps, context, renderLanes2); - suspendIfUpdateReadFromEntangledAsyncAction(); - oldContext = workInProgress2.memoizedState; - unresolvedOldProps || oldState !== oldContext || hasForceUpdate ? ("function" === typeof getDerivedStateFromProps && (applyDerivedStateFromProps( - workInProgress2, - Component2, - getDerivedStateFromProps, - nextProps - ), oldContext = workInProgress2.memoizedState), (oldProps = hasForceUpdate || checkShouldComponentUpdate( - workInProgress2, - Component2, - oldProps, - nextProps, - oldState, - oldContext, - contextType - )) ? (contextType$jscomp$0 || "function" !== typeof context.UNSAFE_componentWillMount && "function" !== typeof context.componentWillMount || ("function" === typeof context.componentWillMount && context.componentWillMount(), "function" === typeof context.UNSAFE_componentWillMount && context.UNSAFE_componentWillMount()), "function" === typeof context.componentDidMount && (workInProgress2.flags |= 4194308)) : ("function" === typeof context.componentDidMount && (workInProgress2.flags |= 4194308), workInProgress2.memoizedProps = nextProps, workInProgress2.memoizedState = oldContext), context.props = nextProps, context.state = oldContext, context.context = contextType, nextProps = oldProps) : ("function" === typeof context.componentDidMount && (workInProgress2.flags |= 4194308), nextProps = false); - } else { - context = workInProgress2.stateNode; - cloneUpdateQueue(current3, workInProgress2); - contextType = workInProgress2.memoizedProps; - contextType$jscomp$0 = resolveClassComponentProps(Component2, contextType); - context.props = contextType$jscomp$0; - getDerivedStateFromProps = workInProgress2.pendingProps; - oldState = context.context; - oldContext = Component2.contextType; - oldProps = emptyContextObject; - "object" === typeof oldContext && null !== oldContext && (oldProps = readContext(oldContext)); - unresolvedOldProps = Component2.getDerivedStateFromProps; - (oldContext = "function" === typeof unresolvedOldProps || "function" === typeof context.getSnapshotBeforeUpdate) || "function" !== typeof context.UNSAFE_componentWillReceiveProps && "function" !== typeof context.componentWillReceiveProps || (contextType !== getDerivedStateFromProps || oldState !== oldProps) && callComponentWillReceiveProps( - workInProgress2, - context, - nextProps, - oldProps - ); - hasForceUpdate = false; - oldState = workInProgress2.memoizedState; - context.state = oldState; - processUpdateQueue(workInProgress2, nextProps, context, renderLanes2); - suspendIfUpdateReadFromEntangledAsyncAction(); - var newState = workInProgress2.memoizedState; - contextType !== getDerivedStateFromProps || oldState !== newState || hasForceUpdate || null !== current3 && null !== current3.dependencies && checkIfContextChanged(current3.dependencies) ? ("function" === typeof unresolvedOldProps && (applyDerivedStateFromProps( - workInProgress2, - Component2, - unresolvedOldProps, - nextProps - ), newState = workInProgress2.memoizedState), (contextType$jscomp$0 = hasForceUpdate || checkShouldComponentUpdate( - workInProgress2, - Component2, - contextType$jscomp$0, - nextProps, - oldState, - newState, - oldProps - ) || null !== current3 && null !== current3.dependencies && checkIfContextChanged(current3.dependencies)) ? (oldContext || "function" !== typeof context.UNSAFE_componentWillUpdate && "function" !== typeof context.componentWillUpdate || ("function" === typeof context.componentWillUpdate && context.componentWillUpdate(nextProps, newState, oldProps), "function" === typeof context.UNSAFE_componentWillUpdate && context.UNSAFE_componentWillUpdate( - nextProps, - newState, - oldProps - )), "function" === typeof context.componentDidUpdate && (workInProgress2.flags |= 4), "function" === typeof context.getSnapshotBeforeUpdate && (workInProgress2.flags |= 1024)) : ("function" !== typeof context.componentDidUpdate || contextType === current3.memoizedProps && oldState === current3.memoizedState || (workInProgress2.flags |= 4), "function" !== typeof context.getSnapshotBeforeUpdate || contextType === current3.memoizedProps && oldState === current3.memoizedState || (workInProgress2.flags |= 1024), workInProgress2.memoizedProps = nextProps, workInProgress2.memoizedState = newState), context.props = nextProps, context.state = newState, context.context = oldProps, nextProps = contextType$jscomp$0) : ("function" !== typeof context.componentDidUpdate || contextType === current3.memoizedProps && oldState === current3.memoizedState || (workInProgress2.flags |= 4), "function" !== typeof context.getSnapshotBeforeUpdate || contextType === current3.memoizedProps && oldState === current3.memoizedState || (workInProgress2.flags |= 1024), nextProps = false); - } - context = nextProps; - markRef(current3, workInProgress2); - nextProps = 0 !== (workInProgress2.flags & 128); - context || nextProps ? (context = workInProgress2.stateNode, Component2 = nextProps && "function" !== typeof Component2.getDerivedStateFromError ? null : context.render(), workInProgress2.flags |= 1, null !== current3 && nextProps ? (workInProgress2.child = reconcileChildFibers( - workInProgress2, - current3.child, - null, - renderLanes2 - ), workInProgress2.child = reconcileChildFibers( - workInProgress2, - null, - Component2, - renderLanes2 - )) : reconcileChildren(current3, workInProgress2, Component2, renderLanes2), workInProgress2.memoizedState = context.state, current3 = workInProgress2.child) : current3 = bailoutOnAlreadyFinishedWork( - current3, - workInProgress2, - renderLanes2 - ); - return current3; - } - function mountHostRootWithoutHydrating(current3, workInProgress2, nextChildren, renderLanes2) { - resetHydrationState(); - workInProgress2.flags |= 256; - reconcileChildren(current3, workInProgress2, nextChildren, renderLanes2); - return workInProgress2.child; - } - var SUSPENDED_MARKER = { - dehydrated: null, - treeContext: null, - retryLane: 0, - hydrationErrors: null - }; - function mountSuspenseOffscreenState(renderLanes2) { - return { baseLanes: renderLanes2, cachePool: getSuspendedCache() }; - } - function getRemainingWorkInPrimaryTree(current3, primaryTreeDidDefer, renderLanes2) { - current3 = null !== current3 ? current3.childLanes & ~renderLanes2 : 0; - primaryTreeDidDefer && (current3 |= workInProgressDeferredLane); - return current3; - } - function updateSuspenseComponent(current3, workInProgress2, renderLanes2) { - var nextProps = workInProgress2.pendingProps, showFallback = false, didSuspend = 0 !== (workInProgress2.flags & 128), JSCompiler_temp; - (JSCompiler_temp = didSuspend) || (JSCompiler_temp = null !== current3 && null === current3.memoizedState ? false : 0 !== (suspenseStackCursor.current & 2)); - JSCompiler_temp && (showFallback = true, workInProgress2.flags &= -129); - JSCompiler_temp = 0 !== (workInProgress2.flags & 32); - workInProgress2.flags &= -33; - if (null === current3) { - if (isHydrating) { - showFallback ? pushPrimaryTreeSuspenseHandler(workInProgress2) : reuseSuspenseHandlerOnStack(); - (current3 = nextHydratableInstance) ? (current3 = canHydrateHydrationBoundary( - current3, - rootOrSingletonContext - ), current3 = null !== current3 && "&" !== current3.data ? current3 : null, null !== current3 && (workInProgress2.memoizedState = { - dehydrated: current3, - treeContext: null !== treeContextProvider ? { id: treeContextId, overflow: treeContextOverflow } : null, - retryLane: 536870912, - hydrationErrors: null - }, renderLanes2 = createFiberFromDehydratedFragment(current3), renderLanes2.return = workInProgress2, workInProgress2.child = renderLanes2, hydrationParentFiber = workInProgress2, nextHydratableInstance = null)) : current3 = null; - if (null === current3) throw throwOnHydrationMismatch(workInProgress2); - isSuspenseInstanceFallback(current3) ? workInProgress2.lanes = 32 : workInProgress2.lanes = 536870912; - return null; - } - var nextPrimaryChildren = nextProps.children; - nextProps = nextProps.fallback; - if (showFallback) - return reuseSuspenseHandlerOnStack(), showFallback = workInProgress2.mode, nextPrimaryChildren = mountWorkInProgressOffscreenFiber( - { mode: "hidden", children: nextPrimaryChildren }, - showFallback - ), nextProps = createFiberFromFragment( - nextProps, - showFallback, - renderLanes2, - null - ), nextPrimaryChildren.return = workInProgress2, nextProps.return = workInProgress2, nextPrimaryChildren.sibling = nextProps, workInProgress2.child = nextPrimaryChildren, nextProps = workInProgress2.child, nextProps.memoizedState = mountSuspenseOffscreenState(renderLanes2), nextProps.childLanes = getRemainingWorkInPrimaryTree( - current3, - JSCompiler_temp, - renderLanes2 - ), workInProgress2.memoizedState = SUSPENDED_MARKER, bailoutOffscreenComponent(null, nextProps); - pushPrimaryTreeSuspenseHandler(workInProgress2); - return mountSuspensePrimaryChildren(workInProgress2, nextPrimaryChildren); - } - var prevState = current3.memoizedState; - if (null !== prevState && (nextPrimaryChildren = prevState.dehydrated, null !== nextPrimaryChildren)) { - if (didSuspend) - workInProgress2.flags & 256 ? (pushPrimaryTreeSuspenseHandler(workInProgress2), workInProgress2.flags &= -257, workInProgress2 = retrySuspenseComponentWithoutHydrating( - current3, - workInProgress2, - renderLanes2 - )) : null !== workInProgress2.memoizedState ? (reuseSuspenseHandlerOnStack(), workInProgress2.child = current3.child, workInProgress2.flags |= 128, workInProgress2 = null) : (reuseSuspenseHandlerOnStack(), nextPrimaryChildren = nextProps.fallback, showFallback = workInProgress2.mode, nextProps = mountWorkInProgressOffscreenFiber( - { mode: "visible", children: nextProps.children }, - showFallback - ), nextPrimaryChildren = createFiberFromFragment( - nextPrimaryChildren, - showFallback, - renderLanes2, - null - ), nextPrimaryChildren.flags |= 2, nextProps.return = workInProgress2, nextPrimaryChildren.return = workInProgress2, nextProps.sibling = nextPrimaryChildren, workInProgress2.child = nextProps, reconcileChildFibers( - workInProgress2, - current3.child, - null, - renderLanes2 - ), nextProps = workInProgress2.child, nextProps.memoizedState = mountSuspenseOffscreenState(renderLanes2), nextProps.childLanes = getRemainingWorkInPrimaryTree( - current3, - JSCompiler_temp, - renderLanes2 - ), workInProgress2.memoizedState = SUSPENDED_MARKER, workInProgress2 = bailoutOffscreenComponent(null, nextProps)); - else if (pushPrimaryTreeSuspenseHandler(workInProgress2), isSuspenseInstanceFallback(nextPrimaryChildren)) { - JSCompiler_temp = nextPrimaryChildren.nextSibling && nextPrimaryChildren.nextSibling.dataset; - if (JSCompiler_temp) var digest = JSCompiler_temp.dgst; - JSCompiler_temp = digest; - nextProps = Error(formatProdErrorMessage(419)); - nextProps.stack = ""; - nextProps.digest = JSCompiler_temp; - queueHydrationError({ value: nextProps, source: null, stack: null }); - workInProgress2 = retrySuspenseComponentWithoutHydrating( - current3, - workInProgress2, - renderLanes2 - ); - } else if (didReceiveUpdate || propagateParentContextChanges(current3, workInProgress2, renderLanes2, false), JSCompiler_temp = 0 !== (renderLanes2 & current3.childLanes), didReceiveUpdate || JSCompiler_temp) { - JSCompiler_temp = workInProgressRoot; - if (null !== JSCompiler_temp && (nextProps = getBumpedLaneForHydration(JSCompiler_temp, renderLanes2), 0 !== nextProps && nextProps !== prevState.retryLane)) - throw prevState.retryLane = nextProps, enqueueConcurrentRenderForLane(current3, nextProps), scheduleUpdateOnFiber(JSCompiler_temp, current3, nextProps), SelectiveHydrationException; - isSuspenseInstancePending(nextPrimaryChildren) || renderDidSuspendDelayIfPossible(); - workInProgress2 = retrySuspenseComponentWithoutHydrating( - current3, - workInProgress2, - renderLanes2 - ); - } else - isSuspenseInstancePending(nextPrimaryChildren) ? (workInProgress2.flags |= 192, workInProgress2.child = current3.child, workInProgress2 = null) : (current3 = prevState.treeContext, nextHydratableInstance = getNextHydratable( - nextPrimaryChildren.nextSibling - ), hydrationParentFiber = workInProgress2, isHydrating = true, hydrationErrors = null, rootOrSingletonContext = false, null !== current3 && restoreSuspendedTreeContext(workInProgress2, current3), workInProgress2 = mountSuspensePrimaryChildren( - workInProgress2, - nextProps.children - ), workInProgress2.flags |= 4096); - return workInProgress2; - } - if (showFallback) - return reuseSuspenseHandlerOnStack(), nextPrimaryChildren = nextProps.fallback, showFallback = workInProgress2.mode, prevState = current3.child, digest = prevState.sibling, nextProps = createWorkInProgress(prevState, { - mode: "hidden", - children: nextProps.children - }), nextProps.subtreeFlags = prevState.subtreeFlags & 65011712, null !== digest ? nextPrimaryChildren = createWorkInProgress( - digest, - nextPrimaryChildren - ) : (nextPrimaryChildren = createFiberFromFragment( - nextPrimaryChildren, - showFallback, - renderLanes2, - null - ), nextPrimaryChildren.flags |= 2), nextPrimaryChildren.return = workInProgress2, nextProps.return = workInProgress2, nextProps.sibling = nextPrimaryChildren, workInProgress2.child = nextProps, bailoutOffscreenComponent(null, nextProps), nextProps = workInProgress2.child, nextPrimaryChildren = current3.child.memoizedState, null === nextPrimaryChildren ? nextPrimaryChildren = mountSuspenseOffscreenState(renderLanes2) : (showFallback = nextPrimaryChildren.cachePool, null !== showFallback ? (prevState = CacheContext._currentValue, showFallback = showFallback.parent !== prevState ? { parent: prevState, pool: prevState } : showFallback) : showFallback = getSuspendedCache(), nextPrimaryChildren = { - baseLanes: nextPrimaryChildren.baseLanes | renderLanes2, - cachePool: showFallback - }), nextProps.memoizedState = nextPrimaryChildren, nextProps.childLanes = getRemainingWorkInPrimaryTree( - current3, - JSCompiler_temp, - renderLanes2 - ), workInProgress2.memoizedState = SUSPENDED_MARKER, bailoutOffscreenComponent(current3.child, nextProps); - pushPrimaryTreeSuspenseHandler(workInProgress2); - renderLanes2 = current3.child; - current3 = renderLanes2.sibling; - renderLanes2 = createWorkInProgress(renderLanes2, { - mode: "visible", - children: nextProps.children - }); - renderLanes2.return = workInProgress2; - renderLanes2.sibling = null; - null !== current3 && (JSCompiler_temp = workInProgress2.deletions, null === JSCompiler_temp ? (workInProgress2.deletions = [current3], workInProgress2.flags |= 16) : JSCompiler_temp.push(current3)); - workInProgress2.child = renderLanes2; - workInProgress2.memoizedState = null; - return renderLanes2; - } - function mountSuspensePrimaryChildren(workInProgress2, primaryChildren) { - primaryChildren = mountWorkInProgressOffscreenFiber( - { mode: "visible", children: primaryChildren }, - workInProgress2.mode - ); - primaryChildren.return = workInProgress2; - return workInProgress2.child = primaryChildren; - } - function mountWorkInProgressOffscreenFiber(offscreenProps, mode) { - offscreenProps = createFiberImplClass(22, offscreenProps, null, mode); - offscreenProps.lanes = 0; - return offscreenProps; - } - function retrySuspenseComponentWithoutHydrating(current3, workInProgress2, renderLanes2) { - reconcileChildFibers(workInProgress2, current3.child, null, renderLanes2); - current3 = mountSuspensePrimaryChildren( - workInProgress2, - workInProgress2.pendingProps.children - ); - current3.flags |= 2; - workInProgress2.memoizedState = null; - return current3; - } - function scheduleSuspenseWorkOnFiber(fiber, renderLanes2, propagationRoot) { - fiber.lanes |= renderLanes2; - var alternate = fiber.alternate; - null !== alternate && (alternate.lanes |= renderLanes2); - scheduleContextWorkOnParentPath(fiber.return, renderLanes2, propagationRoot); - } - function initSuspenseListRenderState(workInProgress2, isBackwards, tail, lastContentRow, tailMode, treeForkCount2) { - var renderState = workInProgress2.memoizedState; - null === renderState ? workInProgress2.memoizedState = { - isBackwards, - rendering: null, - renderingStartTime: 0, - last: lastContentRow, - tail, - tailMode, - treeForkCount: treeForkCount2 - } : (renderState.isBackwards = isBackwards, renderState.rendering = null, renderState.renderingStartTime = 0, renderState.last = lastContentRow, renderState.tail = tail, renderState.tailMode = tailMode, renderState.treeForkCount = treeForkCount2); - } - function updateSuspenseListComponent(current3, workInProgress2, renderLanes2) { - var nextProps = workInProgress2.pendingProps, revealOrder = nextProps.revealOrder, tailMode = nextProps.tail; - nextProps = nextProps.children; - var suspenseContext = suspenseStackCursor.current, shouldForceFallback = 0 !== (suspenseContext & 2); - shouldForceFallback ? (suspenseContext = suspenseContext & 1 | 2, workInProgress2.flags |= 128) : suspenseContext &= 1; - push2(suspenseStackCursor, suspenseContext); - reconcileChildren(current3, workInProgress2, nextProps, renderLanes2); - nextProps = isHydrating ? treeForkCount : 0; - if (!shouldForceFallback && null !== current3 && 0 !== (current3.flags & 128)) - a: for (current3 = workInProgress2.child; null !== current3; ) { - if (13 === current3.tag) - null !== current3.memoizedState && scheduleSuspenseWorkOnFiber(current3, renderLanes2, workInProgress2); - else if (19 === current3.tag) - scheduleSuspenseWorkOnFiber(current3, renderLanes2, workInProgress2); - else if (null !== current3.child) { - current3.child.return = current3; - current3 = current3.child; - continue; - } - if (current3 === workInProgress2) break a; - for (; null === current3.sibling; ) { - if (null === current3.return || current3.return === workInProgress2) - break a; - current3 = current3.return; - } - current3.sibling.return = current3.return; - current3 = current3.sibling; - } - switch (revealOrder) { - case "forwards": - renderLanes2 = workInProgress2.child; - for (revealOrder = null; null !== renderLanes2; ) - current3 = renderLanes2.alternate, null !== current3 && null === findFirstSuspended(current3) && (revealOrder = renderLanes2), renderLanes2 = renderLanes2.sibling; - renderLanes2 = revealOrder; - null === renderLanes2 ? (revealOrder = workInProgress2.child, workInProgress2.child = null) : (revealOrder = renderLanes2.sibling, renderLanes2.sibling = null); - initSuspenseListRenderState( - workInProgress2, - false, - revealOrder, - renderLanes2, - tailMode, - nextProps - ); - break; - case "backwards": - case "unstable_legacy-backwards": - renderLanes2 = null; - revealOrder = workInProgress2.child; - for (workInProgress2.child = null; null !== revealOrder; ) { - current3 = revealOrder.alternate; - if (null !== current3 && null === findFirstSuspended(current3)) { - workInProgress2.child = revealOrder; - break; - } - current3 = revealOrder.sibling; - revealOrder.sibling = renderLanes2; - renderLanes2 = revealOrder; - revealOrder = current3; - } - initSuspenseListRenderState( - workInProgress2, - true, - renderLanes2, - null, - tailMode, - nextProps - ); - break; - case "together": - initSuspenseListRenderState( - workInProgress2, - false, - null, - null, - void 0, - nextProps - ); - break; - default: - workInProgress2.memoizedState = null; - } - return workInProgress2.child; - } - function bailoutOnAlreadyFinishedWork(current3, workInProgress2, renderLanes2) { - null !== current3 && (workInProgress2.dependencies = current3.dependencies); - workInProgressRootSkippedLanes |= workInProgress2.lanes; - if (0 === (renderLanes2 & workInProgress2.childLanes)) - if (null !== current3) { - if (propagateParentContextChanges( - current3, - workInProgress2, - renderLanes2, - false - ), 0 === (renderLanes2 & workInProgress2.childLanes)) - return null; - } else return null; - if (null !== current3 && workInProgress2.child !== current3.child) - throw Error(formatProdErrorMessage(153)); - if (null !== workInProgress2.child) { - current3 = workInProgress2.child; - renderLanes2 = createWorkInProgress(current3, current3.pendingProps); - workInProgress2.child = renderLanes2; - for (renderLanes2.return = workInProgress2; null !== current3.sibling; ) - current3 = current3.sibling, renderLanes2 = renderLanes2.sibling = createWorkInProgress(current3, current3.pendingProps), renderLanes2.return = workInProgress2; - renderLanes2.sibling = null; - } - return workInProgress2.child; - } - function checkScheduledUpdateOrContext(current3, renderLanes2) { - if (0 !== (current3.lanes & renderLanes2)) return true; - current3 = current3.dependencies; - return null !== current3 && checkIfContextChanged(current3) ? true : false; - } - function attemptEarlyBailoutIfNoScheduledUpdate(current3, workInProgress2, renderLanes2) { - switch (workInProgress2.tag) { - case 3: - pushHostContainer(workInProgress2, workInProgress2.stateNode.containerInfo); - pushProvider(workInProgress2, CacheContext, current3.memoizedState.cache); - resetHydrationState(); - break; - case 27: - case 5: - pushHostContext(workInProgress2); - break; - case 4: - pushHostContainer(workInProgress2, workInProgress2.stateNode.containerInfo); - break; - case 10: - pushProvider( - workInProgress2, - workInProgress2.type, - workInProgress2.memoizedProps.value - ); - break; - case 31: - if (null !== workInProgress2.memoizedState) - return workInProgress2.flags |= 128, pushDehydratedActivitySuspenseHandler(workInProgress2), null; - break; - case 13: - var state$102 = workInProgress2.memoizedState; - if (null !== state$102) { - if (null !== state$102.dehydrated) - return pushPrimaryTreeSuspenseHandler(workInProgress2), workInProgress2.flags |= 128, null; - if (0 !== (renderLanes2 & workInProgress2.child.childLanes)) - return updateSuspenseComponent(current3, workInProgress2, renderLanes2); - pushPrimaryTreeSuspenseHandler(workInProgress2); - current3 = bailoutOnAlreadyFinishedWork( - current3, - workInProgress2, - renderLanes2 - ); - return null !== current3 ? current3.sibling : null; - } - pushPrimaryTreeSuspenseHandler(workInProgress2); - break; - case 19: - var didSuspendBefore = 0 !== (current3.flags & 128); - state$102 = 0 !== (renderLanes2 & workInProgress2.childLanes); - state$102 || (propagateParentContextChanges( - current3, - workInProgress2, - renderLanes2, - false - ), state$102 = 0 !== (renderLanes2 & workInProgress2.childLanes)); - if (didSuspendBefore) { - if (state$102) - return updateSuspenseListComponent( - current3, - workInProgress2, - renderLanes2 - ); - workInProgress2.flags |= 128; - } - didSuspendBefore = workInProgress2.memoizedState; - null !== didSuspendBefore && (didSuspendBefore.rendering = null, didSuspendBefore.tail = null, didSuspendBefore.lastEffect = null); - push2(suspenseStackCursor, suspenseStackCursor.current); - if (state$102) break; - else return null; - case 22: - return workInProgress2.lanes = 0, updateOffscreenComponent( - current3, - workInProgress2, - renderLanes2, - workInProgress2.pendingProps - ); - case 24: - pushProvider(workInProgress2, CacheContext, current3.memoizedState.cache); - } - return bailoutOnAlreadyFinishedWork(current3, workInProgress2, renderLanes2); - } - function beginWork(current3, workInProgress2, renderLanes2) { - if (null !== current3) - if (current3.memoizedProps !== workInProgress2.pendingProps) - didReceiveUpdate = true; - else { - if (!checkScheduledUpdateOrContext(current3, renderLanes2) && 0 === (workInProgress2.flags & 128)) - return didReceiveUpdate = false, attemptEarlyBailoutIfNoScheduledUpdate( - current3, - workInProgress2, - renderLanes2 - ); - didReceiveUpdate = 0 !== (current3.flags & 131072) ? true : false; - } - else - didReceiveUpdate = false, isHydrating && 0 !== (workInProgress2.flags & 1048576) && pushTreeId(workInProgress2, treeForkCount, workInProgress2.index); - workInProgress2.lanes = 0; - switch (workInProgress2.tag) { - case 16: - a: { - var props = workInProgress2.pendingProps; - current3 = resolveLazy(workInProgress2.elementType); - workInProgress2.type = current3; - if ("function" === typeof current3) - shouldConstruct(current3) ? (props = resolveClassComponentProps(current3, props), workInProgress2.tag = 1, workInProgress2 = updateClassComponent( - null, - workInProgress2, - current3, - props, - renderLanes2 - )) : (workInProgress2.tag = 0, workInProgress2 = updateFunctionComponent( - null, - workInProgress2, - current3, - props, - renderLanes2 - )); - else { - if (void 0 !== current3 && null !== current3) { - var $$typeof = current3.$$typeof; - if ($$typeof === REACT_FORWARD_REF_TYPE) { - workInProgress2.tag = 11; - workInProgress2 = updateForwardRef( - null, - workInProgress2, - current3, - props, - renderLanes2 - ); - break a; - } else if ($$typeof === REACT_MEMO_TYPE) { - workInProgress2.tag = 14; - workInProgress2 = updateMemoComponent( - null, - workInProgress2, - current3, - props, - renderLanes2 - ); - break a; - } - } - workInProgress2 = getComponentNameFromType(current3) || current3; - throw Error(formatProdErrorMessage(306, workInProgress2, "")); - } - } - return workInProgress2; - case 0: - return updateFunctionComponent( - current3, - workInProgress2, - workInProgress2.type, - workInProgress2.pendingProps, - renderLanes2 - ); - case 1: - return props = workInProgress2.type, $$typeof = resolveClassComponentProps( - props, - workInProgress2.pendingProps - ), updateClassComponent( - current3, - workInProgress2, - props, - $$typeof, - renderLanes2 - ); - case 3: - a: { - pushHostContainer( - workInProgress2, - workInProgress2.stateNode.containerInfo - ); - if (null === current3) throw Error(formatProdErrorMessage(387)); - props = workInProgress2.pendingProps; - var prevState = workInProgress2.memoizedState; - $$typeof = prevState.element; - cloneUpdateQueue(current3, workInProgress2); - processUpdateQueue(workInProgress2, props, null, renderLanes2); - var nextState = workInProgress2.memoizedState; - props = nextState.cache; - pushProvider(workInProgress2, CacheContext, props); - props !== prevState.cache && propagateContextChanges( - workInProgress2, - [CacheContext], - renderLanes2, - true - ); - suspendIfUpdateReadFromEntangledAsyncAction(); - props = nextState.element; - if (prevState.isDehydrated) - if (prevState = { - element: props, - isDehydrated: false, - cache: nextState.cache - }, workInProgress2.updateQueue.baseState = prevState, workInProgress2.memoizedState = prevState, workInProgress2.flags & 256) { - workInProgress2 = mountHostRootWithoutHydrating( - current3, - workInProgress2, - props, - renderLanes2 - ); - break a; - } else if (props !== $$typeof) { - $$typeof = createCapturedValueAtFiber( - Error(formatProdErrorMessage(424)), - workInProgress2 - ); - queueHydrationError($$typeof); - workInProgress2 = mountHostRootWithoutHydrating( - current3, - workInProgress2, - props, - renderLanes2 - ); - break a; - } else { - current3 = workInProgress2.stateNode.containerInfo; - switch (current3.nodeType) { - case 9: - current3 = current3.body; - break; - default: - current3 = "HTML" === current3.nodeName ? current3.ownerDocument.body : current3; - } - nextHydratableInstance = getNextHydratable(current3.firstChild); - hydrationParentFiber = workInProgress2; - isHydrating = true; - hydrationErrors = null; - rootOrSingletonContext = true; - renderLanes2 = mountChildFibers( - workInProgress2, - null, - props, - renderLanes2 - ); - for (workInProgress2.child = renderLanes2; renderLanes2; ) - renderLanes2.flags = renderLanes2.flags & -3 | 4096, renderLanes2 = renderLanes2.sibling; - } - else { - resetHydrationState(); - if (props === $$typeof) { - workInProgress2 = bailoutOnAlreadyFinishedWork( - current3, - workInProgress2, - renderLanes2 - ); - break a; - } - reconcileChildren(current3, workInProgress2, props, renderLanes2); - } - workInProgress2 = workInProgress2.child; - } - return workInProgress2; - case 26: - return markRef(current3, workInProgress2), null === current3 ? (renderLanes2 = getResource( - workInProgress2.type, - null, - workInProgress2.pendingProps, - null - )) ? workInProgress2.memoizedState = renderLanes2 : isHydrating || (renderLanes2 = workInProgress2.type, current3 = workInProgress2.pendingProps, props = getOwnerDocumentFromRootContainer( - rootInstanceStackCursor.current - ).createElement(renderLanes2), props[internalInstanceKey] = workInProgress2, props[internalPropsKey] = current3, setInitialProperties(props, renderLanes2, current3), markNodeAsHoistable(props), workInProgress2.stateNode = props) : workInProgress2.memoizedState = getResource( - workInProgress2.type, - current3.memoizedProps, - workInProgress2.pendingProps, - current3.memoizedState - ), null; - case 27: - return pushHostContext(workInProgress2), null === current3 && isHydrating && (props = workInProgress2.stateNode = resolveSingletonInstance( - workInProgress2.type, - workInProgress2.pendingProps, - rootInstanceStackCursor.current - ), hydrationParentFiber = workInProgress2, rootOrSingletonContext = true, $$typeof = nextHydratableInstance, isSingletonScope(workInProgress2.type) ? (previousHydratableOnEnteringScopedSingleton = $$typeof, nextHydratableInstance = getNextHydratable(props.firstChild)) : nextHydratableInstance = $$typeof), reconcileChildren( - current3, - workInProgress2, - workInProgress2.pendingProps.children, - renderLanes2 - ), markRef(current3, workInProgress2), null === current3 && (workInProgress2.flags |= 4194304), workInProgress2.child; - case 5: - if (null === current3 && isHydrating) { - if ($$typeof = props = nextHydratableInstance) - props = canHydrateInstance( - props, - workInProgress2.type, - workInProgress2.pendingProps, - rootOrSingletonContext - ), null !== props ? (workInProgress2.stateNode = props, hydrationParentFiber = workInProgress2, nextHydratableInstance = getNextHydratable(props.firstChild), rootOrSingletonContext = false, $$typeof = true) : $$typeof = false; - $$typeof || throwOnHydrationMismatch(workInProgress2); - } - pushHostContext(workInProgress2); - $$typeof = workInProgress2.type; - prevState = workInProgress2.pendingProps; - nextState = null !== current3 ? current3.memoizedProps : null; - props = prevState.children; - shouldSetTextContent($$typeof, prevState) ? props = null : null !== nextState && shouldSetTextContent($$typeof, nextState) && (workInProgress2.flags |= 32); - null !== workInProgress2.memoizedState && ($$typeof = renderWithHooks( - current3, - workInProgress2, - TransitionAwareHostComponent, - null, - null, - renderLanes2 - ), HostTransitionContext._currentValue = $$typeof); - markRef(current3, workInProgress2); - reconcileChildren(current3, workInProgress2, props, renderLanes2); - return workInProgress2.child; - case 6: - if (null === current3 && isHydrating) { - if (current3 = renderLanes2 = nextHydratableInstance) - renderLanes2 = canHydrateTextInstance( - renderLanes2, - workInProgress2.pendingProps, - rootOrSingletonContext - ), null !== renderLanes2 ? (workInProgress2.stateNode = renderLanes2, hydrationParentFiber = workInProgress2, nextHydratableInstance = null, current3 = true) : current3 = false; - current3 || throwOnHydrationMismatch(workInProgress2); - } - return null; - case 13: - return updateSuspenseComponent(current3, workInProgress2, renderLanes2); - case 4: - return pushHostContainer( - workInProgress2, - workInProgress2.stateNode.containerInfo - ), props = workInProgress2.pendingProps, null === current3 ? workInProgress2.child = reconcileChildFibers( - workInProgress2, - null, - props, - renderLanes2 - ) : reconcileChildren(current3, workInProgress2, props, renderLanes2), workInProgress2.child; - case 11: - return updateForwardRef( - current3, - workInProgress2, - workInProgress2.type, - workInProgress2.pendingProps, - renderLanes2 - ); - case 7: - return reconcileChildren( - current3, - workInProgress2, - workInProgress2.pendingProps, - renderLanes2 - ), workInProgress2.child; - case 8: - return reconcileChildren( - current3, - workInProgress2, - workInProgress2.pendingProps.children, - renderLanes2 - ), workInProgress2.child; - case 12: - return reconcileChildren( - current3, - workInProgress2, - workInProgress2.pendingProps.children, - renderLanes2 - ), workInProgress2.child; - case 10: - return props = workInProgress2.pendingProps, pushProvider(workInProgress2, workInProgress2.type, props.value), reconcileChildren(current3, workInProgress2, props.children, renderLanes2), workInProgress2.child; - case 9: - return $$typeof = workInProgress2.type._context, props = workInProgress2.pendingProps.children, prepareToReadContext(workInProgress2), $$typeof = readContext($$typeof), props = props($$typeof), workInProgress2.flags |= 1, reconcileChildren(current3, workInProgress2, props, renderLanes2), workInProgress2.child; - case 14: - return updateMemoComponent( - current3, - workInProgress2, - workInProgress2.type, - workInProgress2.pendingProps, - renderLanes2 - ); - case 15: - return updateSimpleMemoComponent( - current3, - workInProgress2, - workInProgress2.type, - workInProgress2.pendingProps, - renderLanes2 - ); - case 19: - return updateSuspenseListComponent(current3, workInProgress2, renderLanes2); - case 31: - return updateActivityComponent(current3, workInProgress2, renderLanes2); - case 22: - return updateOffscreenComponent( - current3, - workInProgress2, - renderLanes2, - workInProgress2.pendingProps - ); - case 24: - return prepareToReadContext(workInProgress2), props = readContext(CacheContext), null === current3 ? ($$typeof = peekCacheFromPool(), null === $$typeof && ($$typeof = workInProgressRoot, prevState = createCache(), $$typeof.pooledCache = prevState, prevState.refCount++, null !== prevState && ($$typeof.pooledCacheLanes |= renderLanes2), $$typeof = prevState), workInProgress2.memoizedState = { parent: props, cache: $$typeof }, initializeUpdateQueue(workInProgress2), pushProvider(workInProgress2, CacheContext, $$typeof)) : (0 !== (current3.lanes & renderLanes2) && (cloneUpdateQueue(current3, workInProgress2), processUpdateQueue(workInProgress2, null, null, renderLanes2), suspendIfUpdateReadFromEntangledAsyncAction()), $$typeof = current3.memoizedState, prevState = workInProgress2.memoizedState, $$typeof.parent !== props ? ($$typeof = { parent: props, cache: props }, workInProgress2.memoizedState = $$typeof, 0 === workInProgress2.lanes && (workInProgress2.memoizedState = workInProgress2.updateQueue.baseState = $$typeof), pushProvider(workInProgress2, CacheContext, props)) : (props = prevState.cache, pushProvider(workInProgress2, CacheContext, props), props !== $$typeof.cache && propagateContextChanges( - workInProgress2, - [CacheContext], - renderLanes2, - true - ))), reconcileChildren( - current3, - workInProgress2, - workInProgress2.pendingProps.children, - renderLanes2 - ), workInProgress2.child; - case 29: - throw workInProgress2.pendingProps; - } - throw Error(formatProdErrorMessage(156, workInProgress2.tag)); - } - function markUpdate(workInProgress2) { - workInProgress2.flags |= 4; - } - function preloadInstanceAndSuspendIfNeeded(workInProgress2, type, oldProps, newProps, renderLanes2) { - if (type = 0 !== (workInProgress2.mode & 32)) type = false; - if (type) { - if (workInProgress2.flags |= 16777216, (renderLanes2 & 335544128) === renderLanes2) - if (workInProgress2.stateNode.complete) workInProgress2.flags |= 8192; - else if (shouldRemainOnPreviousScreen()) workInProgress2.flags |= 8192; - else - throw suspendedThenable = noopSuspenseyCommitThenable, SuspenseyCommitException; - } else workInProgress2.flags &= -16777217; - } - function preloadResourceAndSuspendIfNeeded(workInProgress2, resource) { - if ("stylesheet" !== resource.type || 0 !== (resource.state.loading & 4)) - workInProgress2.flags &= -16777217; - else if (workInProgress2.flags |= 16777216, !preloadResource(resource)) - if (shouldRemainOnPreviousScreen()) workInProgress2.flags |= 8192; - else - throw suspendedThenable = noopSuspenseyCommitThenable, SuspenseyCommitException; - } - function scheduleRetryEffect(workInProgress2, retryQueue) { - null !== retryQueue && (workInProgress2.flags |= 4); - workInProgress2.flags & 16384 && (retryQueue = 22 !== workInProgress2.tag ? claimNextRetryLane() : 536870912, workInProgress2.lanes |= retryQueue, workInProgressSuspendedRetryLanes |= retryQueue); - } - function cutOffTailIfNeeded(renderState, hasRenderedATailFallback) { - if (!isHydrating) - switch (renderState.tailMode) { - case "hidden": - hasRenderedATailFallback = renderState.tail; - for (var lastTailNode = null; null !== hasRenderedATailFallback; ) - null !== hasRenderedATailFallback.alternate && (lastTailNode = hasRenderedATailFallback), hasRenderedATailFallback = hasRenderedATailFallback.sibling; - null === lastTailNode ? renderState.tail = null : lastTailNode.sibling = null; - break; - case "collapsed": - lastTailNode = renderState.tail; - for (var lastTailNode$106 = null; null !== lastTailNode; ) - null !== lastTailNode.alternate && (lastTailNode$106 = lastTailNode), lastTailNode = lastTailNode.sibling; - null === lastTailNode$106 ? hasRenderedATailFallback || null === renderState.tail ? renderState.tail = null : renderState.tail.sibling = null : lastTailNode$106.sibling = null; - } - } - function bubbleProperties(completedWork) { - var didBailout = null !== completedWork.alternate && completedWork.alternate.child === completedWork.child, newChildLanes = 0, subtreeFlags = 0; - if (didBailout) - for (var child$107 = completedWork.child; null !== child$107; ) - newChildLanes |= child$107.lanes | child$107.childLanes, subtreeFlags |= child$107.subtreeFlags & 65011712, subtreeFlags |= child$107.flags & 65011712, child$107.return = completedWork, child$107 = child$107.sibling; - else - for (child$107 = completedWork.child; null !== child$107; ) - newChildLanes |= child$107.lanes | child$107.childLanes, subtreeFlags |= child$107.subtreeFlags, subtreeFlags |= child$107.flags, child$107.return = completedWork, child$107 = child$107.sibling; - completedWork.subtreeFlags |= subtreeFlags; - completedWork.childLanes = newChildLanes; - return didBailout; - } - function completeWork(current3, workInProgress2, renderLanes2) { - var newProps = workInProgress2.pendingProps; - popTreeContext(workInProgress2); - switch (workInProgress2.tag) { - case 16: - case 15: - case 0: - case 11: - case 7: - case 8: - case 12: - case 9: - case 14: - return bubbleProperties(workInProgress2), null; - case 1: - return bubbleProperties(workInProgress2), null; - case 3: - renderLanes2 = workInProgress2.stateNode; - newProps = null; - null !== current3 && (newProps = current3.memoizedState.cache); - workInProgress2.memoizedState.cache !== newProps && (workInProgress2.flags |= 2048); - popProvider(CacheContext); - popHostContainer(); - renderLanes2.pendingContext && (renderLanes2.context = renderLanes2.pendingContext, renderLanes2.pendingContext = null); - if (null === current3 || null === current3.child) - popHydrationState(workInProgress2) ? markUpdate(workInProgress2) : null === current3 || current3.memoizedState.isDehydrated && 0 === (workInProgress2.flags & 256) || (workInProgress2.flags |= 1024, upgradeHydrationErrorsToRecoverable()); - bubbleProperties(workInProgress2); - return null; - case 26: - var type = workInProgress2.type, nextResource = workInProgress2.memoizedState; - null === current3 ? (markUpdate(workInProgress2), null !== nextResource ? (bubbleProperties(workInProgress2), preloadResourceAndSuspendIfNeeded(workInProgress2, nextResource)) : (bubbleProperties(workInProgress2), preloadInstanceAndSuspendIfNeeded( - workInProgress2, - type, - null, - newProps, - renderLanes2 - ))) : nextResource ? nextResource !== current3.memoizedState ? (markUpdate(workInProgress2), bubbleProperties(workInProgress2), preloadResourceAndSuspendIfNeeded(workInProgress2, nextResource)) : (bubbleProperties(workInProgress2), workInProgress2.flags &= -16777217) : (current3 = current3.memoizedProps, current3 !== newProps && markUpdate(workInProgress2), bubbleProperties(workInProgress2), preloadInstanceAndSuspendIfNeeded( - workInProgress2, - type, - current3, - newProps, - renderLanes2 - )); - return null; - case 27: - popHostContext(workInProgress2); - renderLanes2 = rootInstanceStackCursor.current; - type = workInProgress2.type; - if (null !== current3 && null != workInProgress2.stateNode) - current3.memoizedProps !== newProps && markUpdate(workInProgress2); - else { - if (!newProps) { - if (null === workInProgress2.stateNode) - throw Error(formatProdErrorMessage(166)); - bubbleProperties(workInProgress2); - return null; - } - current3 = contextStackCursor.current; - popHydrationState(workInProgress2) ? prepareToHydrateHostInstance(workInProgress2) : (current3 = resolveSingletonInstance(type, newProps, renderLanes2), workInProgress2.stateNode = current3, markUpdate(workInProgress2)); - } - bubbleProperties(workInProgress2); - return null; - case 5: - popHostContext(workInProgress2); - type = workInProgress2.type; - if (null !== current3 && null != workInProgress2.stateNode) - current3.memoizedProps !== newProps && markUpdate(workInProgress2); - else { - if (!newProps) { - if (null === workInProgress2.stateNode) - throw Error(formatProdErrorMessage(166)); - bubbleProperties(workInProgress2); - return null; - } - nextResource = contextStackCursor.current; - if (popHydrationState(workInProgress2)) - prepareToHydrateHostInstance(workInProgress2); - else { - var ownerDocument = getOwnerDocumentFromRootContainer( - rootInstanceStackCursor.current - ); - switch (nextResource) { - case 1: - nextResource = ownerDocument.createElementNS( - "http://www.w3.org/2000/svg", - type - ); - break; - case 2: - nextResource = ownerDocument.createElementNS( - "http://www.w3.org/1998/Math/MathML", - type - ); - break; - default: - switch (type) { - case "svg": - nextResource = ownerDocument.createElementNS( - "http://www.w3.org/2000/svg", - type - ); - break; - case "math": - nextResource = ownerDocument.createElementNS( - "http://www.w3.org/1998/Math/MathML", - type - ); - break; - case "script": - nextResource = ownerDocument.createElement("div"); - nextResource.innerHTML = " - - - - - - - -
- - diff --git a/src/inspect_scout/_view/www/e2e/app-navigation.spec.ts b/src/inspect_scout/_view/www/e2e/app-navigation.spec.ts deleted file mode 100644 index a6e2f766d..000000000 --- a/src/inspect_scout/_view/www/e2e/app-navigation.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { test, expect } from "./fixtures/app"; - -test("activity bar renders with expected items", async ({ page }) => { - await page.goto("/"); - - await expect(page.locator("#project")).toBeVisible(); - await expect(page.locator("#transcripts")).toBeVisible(); - await expect(page.locator("#scans")).toBeVisible(); - await expect(page.locator("#validation")).toBeVisible(); -}); - -test("default route is /transcripts when transcripts dir exists", async ({ - page, -}) => { - await page.goto("/"); - - // Wait for the redirect to complete - await expect(page).toHaveURL(/#\/transcripts/); -}); - -test("clicking activity bar items navigates to correct routes", async ({ - page, -}) => { - await page.goto("/"); - await expect(page).toHaveURL(/#\/transcripts/); - - await page.locator("#project").click(); - await expect(page).toHaveURL(/#\/project/); - - await page.locator("#scans").click(); - await expect(page).toHaveURL(/#\/scans/); - - await page.locator("#validation").click(); - await expect(page).toHaveURL(/#\/validation/); - - await page.locator("#transcripts").click(); - await expect(page).toHaveURL(/#\/transcripts/); -}); diff --git a/src/inspect_scout/_view/www/e2e/fixtures/app.ts b/src/inspect_scout/_view/www/e2e/fixtures/app.ts deleted file mode 100644 index ebe10c2ae..000000000 --- a/src/inspect_scout/_view/www/e2e/fixtures/app.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - createNetworkFixture, - type NetworkFixture, -} from "@msw/playwright"; -import { test as base } from "@playwright/test"; - -import { defaultHandlers } from "./handlers"; - -/** - * MockEventSource script injected via page.addInitScript(). - * - * Immediately dispatches an SSE message with topic versions so the app's - * useTopicInvalidation hook unblocks and the UI renders. - */ -const MOCK_EVENT_SOURCE_SCRIPT = ` - window.EventSource = class MockEventSource { - static CONNECTING = 0; - static OPEN = 1; - static CLOSED = 2; - - readyState = 0; - onopen = null; - onmessage = null; - onerror = null; - url; - - constructor(url) { - this.url = url; - // Transition to OPEN and dispatch initial topic versions on next microtask - Promise.resolve().then(() => { - this.readyState = 1; - if (this.onopen) this.onopen(new Event("open")); - if (this.onmessage) { - this.onmessage( - new MessageEvent("message", { - data: JSON.stringify({ scans: "t1", "project-config": "p1" }), - }), - ); - } - }); - } - - close() { - this.readyState = 2; - } - - addEventListener() {} - removeEventListener() {} - dispatchEvent() { return true; } - }; -`; - -interface AppFixtures { - network: NetworkFixture; - disableRetries: void; -} - -export const test = base.extend({ - // Override page to inject SSE mock before any navigation - page: async ({ page }, use) => { - await page.addInitScript(MOCK_EVENT_SOURCE_SCRIPT); - await use(page); // eslint-disable-line react-hooks/rules-of-hooks - }, - - // Wire up MSW handlers via @msw/playwright - network: createNetworkFixture({ - initialHandlers: defaultHandlers, - }), - - // Opt-in fixture: destructure in a test to disable React Query retries - disableRetries: [ - async ({ page }, use) => { - await page.addInitScript(() => { - globalThis.__TEST_DISABLE_RETRY = true; - }); - await use(); - }, - { auto: false }, - ], -}); - -export { expect } from "@playwright/test"; diff --git a/src/inspect_scout/_view/www/e2e/fixtures/handlers.ts b/src/inspect_scout/_view/www/e2e/fixtures/handlers.ts deleted file mode 100644 index efc6dd8c8..000000000 --- a/src/inspect_scout/_view/www/e2e/fixtures/handlers.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { http, HttpResponse } from "msw"; - -import type { - ActiveScansResponse, - AppConfig, - MessagesEventsResponse, - ProjectConfig, - ScansResponse, - Status, - TranscriptInfo, - TranscriptsResponse, -} from "../../src/types/api-types"; - -import { - createActiveScansResponse, - createAppConfig, - createMessagesEventsResponse, - createProjectConfig, - createScansResponse, - createStatus, - createTranscriptInfo, - createTranscriptsResponse, -} from "./test-data"; - -/** Default handlers that let the app boot cleanly. */ -export const defaultHandlers = [ - http.get("/api/v2/app-config", () => { - return HttpResponse.json(createAppConfig()); - }), - - http.post("/api/v2/transcripts/:dir", () => { - return HttpResponse.json(createTranscriptsResponse()); - }), - - http.post("/api/v2/scans/:dir", () => { - return HttpResponse.json(createScansResponse()); - }), - - http.get("/api/v2/scans/active", () => { - return HttpResponse.json(createActiveScansResponse()); - }), - - http.post("/api/v2/transcripts/:dir/distinct", () => { - return HttpResponse.json([]); - }), - - http.post("/api/v2/scans/:dir/distinct", () => { - return HttpResponse.json([]); - }), - - http.get("/api/v2/validations", () => { - return HttpResponse.json([]); - }), - - http.get("/api/v2/project/config", () => { - return HttpResponse.json(createProjectConfig(), { - headers: { ETag: '"e2e-etag-1"' }, - }); - }), - - // Detail panel defaults (prevent unhandled requests during unrelated tests) - http.get("/api/v2/scans/:dir/:scanPath", () => { - return HttpResponse.json(createStatus()); - }), - - http.get("/api/v2/transcripts/:dir/:id/info", () => { - return HttpResponse.json( - createTranscriptInfo({ transcript_id: "default" }), - ); - }), - - http.get("/api/v2/transcripts/:dir/:id/messages-events", () => { - return HttpResponse.json( - createMessagesEventsResponse(), - ); - }), -]; diff --git a/src/inspect_scout/_view/www/e2e/fixtures/test-data.ts b/src/inspect_scout/_view/www/e2e/fixtures/test-data.ts deleted file mode 100644 index e9965470a..000000000 --- a/src/inspect_scout/_view/www/e2e/fixtures/test-data.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { - ActiveScansResponse, - AppConfig, - MessagesEventsResponse, - ProjectConfig, - ScanRow, - ScansResponse, - Status, - TranscriptInfo, - TranscriptsResponse, -} from "../../src/types/api-types"; - -export function createAppConfig( - overrides?: Partial, -): AppConfig { - return { - home_dir: "/home/test", - project_dir: "/home/test/project", - filter: [], - scans: { dir: "/home/test/project/.scans", source: "project" }, - transcripts: { - dir: "/home/test/project/.transcripts", - source: "project", - }, - ...overrides, - } satisfies AppConfig; -} - -export function createTranscriptInfo( - overrides: Partial & { transcript_id: string }, -): TranscriptInfo { - return { - metadata: {}, - ...overrides, - }; -} - -export function createScanRow( - overrides: Partial & { scan_id: string }, -): ScanRow { - return { - location: `/scans/${overrides.scan_id}`, - packages: {}, - scan_name: overrides.scan_id, - scanners: "", - status: "complete", - tags: "", - timestamp: "2024-01-01T00:00:00Z", - total_errors: 0, - total_results: 0, - total_tokens: 0, - transcript_count: 0, - ...overrides, - }; -} - -export function createTranscriptsResponse( - items: TranscriptInfo[] = [], -): TranscriptsResponse { - return { - items, - next_cursor: null, - total_count: items.length, - } satisfies TranscriptsResponse; -} - -export function createScansResponse( - items: ScanRow[] = [], -): ScansResponse { - return { - items, - next_cursor: null, - total_count: items.length, - } satisfies ScansResponse; -} - -export function createActiveScansResponse(): ActiveScansResponse { - return { - items: {}, - } satisfies ActiveScansResponse; -} - -export function createProjectConfig(): ProjectConfig { - return { - filter: [], - } satisfies ProjectConfig; -} - -export function createStatus(overrides?: Partial): Status { - return { - complete: true, - errors: [], - location: - "/home/test/project/.scans/scan_id=aBcDeFgHiJkLmNoPqRsTuV", - spec: { - scan_id: "aBcDeFgHiJkLmNoPqRsTuV", - scan_name: "eval-safety", - options: { max_transcripts: 25 }, - packages: {}, - scanners: {}, - timestamp: "2024-01-01T00:00:00Z", - }, - summary: { complete: true, scanners: {} }, - ...overrides, - } satisfies Status; -} - -export function createMessagesEventsResponse( - overrides?: Partial, -): MessagesEventsResponse { - return { - messages: [], - events: [], - ...overrides, - } satisfies MessagesEventsResponse; -} diff --git a/src/inspect_scout/_view/www/e2e/scan.spec.ts b/src/inspect_scout/_view/www/e2e/scan.spec.ts deleted file mode 100644 index 00270b2b1..000000000 --- a/src/inspect_scout/_view/www/e2e/scan.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { http, HttpResponse } from "msw"; - -import type { ScansResponse, Status } from "../src/types/api-types"; -import { encodeBase64Url } from "../src/utils/base64url"; - -import { test, expect } from "./fixtures/app"; -import { - createScanRow, - createScansResponse, - createStatus, -} from "./fixtures/test-data"; - -const SCANS_DIR = "/home/test/project/.scans"; -const SCAN_ID = "aBcDeFgHiJkLmNoPqRsTuV"; -const SCAN_LOCATION = `${SCANS_DIR}/scan_id=${SCAN_ID}`; -const SCAN_RELATIVE_PATH = `scan_id=${SCAN_ID}`; - -test("clicking a scan row opens the scan detail panel", async ({ - page, - network, -}) => { - network.use( - http.post("*/api/v2/scans/:dir", () => - HttpResponse.json( - createScansResponse([ - createScanRow({ - scan_id: SCAN_ID, - scan_name: "eval-safety", - location: SCAN_LOCATION, - }), - ]), - ), - ), - http.get("*/api/v2/scans/:dir/:scanPath", () => - HttpResponse.json( - createStatus({ - location: SCAN_LOCATION, - spec: { - scan_id: SCAN_ID, - scan_name: "eval-safety", - options: { max_transcripts: 25 }, - packages: {}, - scanners: {}, - timestamp: "2024-01-15T10:30:00Z", - }, - }), - ), - ), - ); - - await page.goto("/#/scans"); - await page.getByText("eval-safety").first().click(); - - await expect(page.locator("h1")).toContainText("eval-safety"); - await expect(page.getByText("Complete")).toBeVisible(); -}); - -test("scan panel shows error state when scan API fails", async ({ - page, - network, - disableRetries: _, -}) => { - network.use( - http.get("*/api/v2/scans/:dir/:scanPath", () => - HttpResponse.text("Internal Server Error", { status: 500 }), - ), - ); - - const encodedDir = encodeBase64Url(SCANS_DIR); - await page.goto(`/#/scan/${encodedDir}/${SCAN_RELATIVE_PATH}`); - - await expect(page.getByText("Error Loading Scan")).toBeVisible(); -}); diff --git a/src/inspect_scout/_view/www/e2e/scans.spec.ts b/src/inspect_scout/_view/www/e2e/scans.spec.ts deleted file mode 100644 index 989ebf933..000000000 --- a/src/inspect_scout/_view/www/e2e/scans.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { http, HttpResponse } from "msw"; - -import type { ScansResponse } from "../src/types/api-types"; - -import { test, expect } from "./fixtures/app"; -import { createScanRow, createScansResponse } from "./fixtures/test-data"; - -test("scans page renders grid with data", async ({ page, network }) => { - network.use( - http.post("*/api/v2/scans/:dir", () => - HttpResponse.json( - createScansResponse([ - createScanRow({ - scan_id: "scan-001", - scan_name: "eval-safety", - status: "complete", - total_results: 42, - }), - createScanRow({ - scan_id: "scan-002", - scan_name: "eval-quality", - status: "active", - total_results: 10, - }), - ]), - ), - ), - ); - - await page.goto("/#/scans"); - - // Grid renders with scan data - await expect(page.getByText("eval-safety").first()).toBeVisible(); - await expect(page.getByText("eval-quality").first()).toBeVisible(); - - // Footer shows item count - await expect(page.locator("#scan-job-footer")).toContainText("2 items"); -}); - -test("scans page shows empty state when no scans exist", async ({ page }) => { - await page.goto("/#/scans"); - - // Footer shows 0 items - await expect(page.locator("#scan-job-footer")).toContainText("0 items"); -}); - -test("scans page shows error panel on API failure", async ({ - page, - network, - disableRetries: _, -}) => { - network.use( - http.post("*/api/v2/scans/:dir", () => - HttpResponse.text("Internal Server Error", { status: 500 }), - ), - ); - - await page.goto("/#/scans"); - - await expect(page.getByText("Error Loading Scans")).toBeVisible(); -}); diff --git a/src/inspect_scout/_view/www/e2e/transcript.spec.ts b/src/inspect_scout/_view/www/e2e/transcript.spec.ts deleted file mode 100644 index d1380d045..000000000 --- a/src/inspect_scout/_view/www/e2e/transcript.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { http, HttpResponse } from "msw"; - -import type { - MessagesEventsResponse, - TranscriptInfo, - TranscriptsResponse, -} from "../src/types/api-types"; -import { encodeBase64Url } from "../src/utils/base64url"; - -import { test, expect } from "./fixtures/app"; -import { - createMessagesEventsResponse, - createTranscriptInfo, - createTranscriptsResponse, -} from "./fixtures/test-data"; - -const TRANSCRIPTS_DIR = "/home/test/project/.transcripts"; -const TRANSCRIPT_ID = "t-001"; - -test("clicking a transcript row opens the transcript detail panel", async ({ - page, - network, -}) => { - network.use( - http.post("*/api/v2/transcripts/:dir", () => - HttpResponse.json( - createTranscriptsResponse([ - createTranscriptInfo({ - transcript_id: TRANSCRIPT_ID, - task_id: "my-task", - model: "claude-3", - date: "2024-01-15T10:30:00Z", - }), - ]), - ), - ), - http.get("*/api/v2/transcripts/:dir/:id/info", () => - HttpResponse.json( - createTranscriptInfo({ - transcript_id: TRANSCRIPT_ID, - task_id: "my-task", - model: "claude-3", - date: "2024-01-15T10:30:00Z", - }), - ), - ), - http.get("*/api/v2/transcripts/:dir/:id/messages-events", () => - HttpResponse.json(createMessagesEventsResponse()), - ), - ); - - await page.goto("/#/transcripts"); - await page.getByText("my-task").first().click(); - - await expect(page.getByText("my-task").first()).toBeVisible(); - await expect(page.getByText("claude-3")).toBeVisible(); -}); - -test("transcript panel shows error state when API fails", async ({ - page, - network, - disableRetries: _, -}) => { - network.use( - http.get("*/api/v2/transcripts/:dir/:id/info", () => - HttpResponse.text("Internal Server Error", { status: 500 }), - ), - ); - - const encodedDir = encodeBase64Url(TRANSCRIPTS_DIR); - await page.goto( - `/#/transcripts/${encodedDir}/${TRANSCRIPT_ID}`, - ); - - await expect(page.getByText("Error Loading Transcript")).toBeVisible(); -}); diff --git a/src/inspect_scout/_view/www/e2e/transcripts.spec.ts b/src/inspect_scout/_view/www/e2e/transcripts.spec.ts deleted file mode 100644 index d65fccbb1..000000000 --- a/src/inspect_scout/_view/www/e2e/transcripts.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { http, HttpResponse } from "msw"; - -import type { TranscriptsResponse } from "../src/types/api-types"; - -import { test, expect } from "./fixtures/app"; -import { - createTranscriptInfo, - createTranscriptsResponse, -} from "./fixtures/test-data"; - -test("transcripts page renders grid with data", async ({ page, network }) => { - network.use( - http.post("*/api/v2/transcripts/:dir", () => - HttpResponse.json( - createTranscriptsResponse([ - createTranscriptInfo({ - transcript_id: "t-001", - task_id: "my-task", - model: "claude-3", - success: true, - }), - createTranscriptInfo({ - transcript_id: "t-002", - task_id: "other-task", - model: "gpt-4", - success: false, - }), - ]), - ), - ), - ); - - await page.goto("/#/transcripts"); - - // Grid renders with transcript data - await expect(page.getByText("my-task").first()).toBeVisible(); - await expect(page.getByText("other-task").first()).toBeVisible(); - - // Footer shows item count - await expect(page.locator("#transcripts-footer")).toContainText("2 items"); -}); - -test("transcripts page shows empty state when no transcripts exist", async ({ - page, -}) => { - await page.goto("/#/transcripts"); - - // Footer shows 0 items - await expect(page.locator("#transcripts-footer")).toContainText("0 items"); -}); - -test("transcripts page shows error panel on API failure", async ({ - page, - network, - disableRetries: _, -}) => { - network.use( - http.post("*/api/v2/transcripts/:dir", () => - HttpResponse.text("Internal Server Error", { status: 500 }), - ), - ); - - await page.goto("/#/transcripts"); - - await expect(page.getByText("Error Loading Transcript")).toBeVisible(); -}); diff --git a/src/inspect_scout/_view/www/eslint.config.js b/src/inspect_scout/_view/www/eslint.config.js deleted file mode 100644 index ff0b1bec3..000000000 --- a/src/inspect_scout/_view/www/eslint.config.js +++ /dev/null @@ -1,103 +0,0 @@ -import js from "@eslint/js"; -import tseslint from "typescript-eslint"; -import reactPlugin from "eslint-plugin-react"; -import reactHooksPlugin from "eslint-plugin-react-hooks"; -import importPlugin from "eslint-plugin-import"; -import reactRefreshPlugin from "eslint-plugin-react-refresh"; -import prettierConfig from "eslint-config-prettier"; - -export default tseslint.config( - { - ignores: [ - "dist/", - "node_modules/", - "build/", - "scripts/", - "playwright-report/", - "test-results/", - "*.config.?s", - "*.config.cjs", - "src/types/generated.ts", - ], - }, - js.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - //...tseslint.configs.strictTypeChecked, - { - languageOptions: { - parserOptions: { - projectService: { - allowDefaultProject: ["*.config.js", "*.config.ts", "*.config.cjs"], - }, - tsconfigRootDir: import.meta.dirname, - }, - }, - }, - { - files: ["**/*.{ts,tsx}"], - plugins: { - react: reactPlugin, - "react-hooks": reactHooksPlugin, - "react-refresh": reactRefreshPlugin, - import: importPlugin, - }, - rules: { - "import/order": [ - "error", - { - groups: [ - "builtin", - "external", - "internal", - "parent", - "sibling", - "index", - ], - "newlines-between": "always", - alphabetize: { - order: "asc", - caseInsensitive: true, - }, - }, - ], - // These are disabled because we didn't have time to fix them, not because they are bad rules - "no-unused-vars": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-redundant-type-constituents": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-base-to-string": "off", - "@typescript-eslint/unbound-method": "off", - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-unused-vars": "off", - // "@typescript-eslint/no-unused-vars": [ - // "error", - // { varsIgnorePattern: "^_" }, - // ] - ...reactPlugin.configs.recommended.rules, - ...reactPlugin.configs["jsx-runtime"].rules, - "react/prop-types": "off", - "react/display-name": "off", - "react/no-children-prop": "off", - "react/no-unescaped-entities": "off", - ...reactHooksPlugin.configs.recommended.rules, - // ...reactRefreshPlugin.configs.recommended.rules, - // // We may want to remove the disables below as we see fit - // "@typescript-eslint/restrict-template-expressions": "off", - // "@typescript-eslint/no-unnecessary-type-parameters": "off", - }, - // "react/prop-types": "off", - settings: { - react: { - version: "detect", - }, - "import/resolver": { - typescript: true, - node: true, - }, - }, - }, - prettierConfig -); diff --git a/src/inspect_scout/_view/www/favicon.svg b/src/inspect_scout/_view/www/favicon.svg deleted file mode 100644 index 95ca1a99b..000000000 --- a/src/inspect_scout/_view/www/favicon.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/inspect_scout/_view/www/index.html b/src/inspect_scout/_view/www/index.html deleted file mode 100644 index 0b5837a0c..000000000 --- a/src/inspect_scout/_view/www/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - Inspect Scout - - - - - - -
- - - diff --git a/src/inspect_scout/_view/www/meridianlabs-inspect-scout-viewer-0.0.1.tgz b/src/inspect_scout/_view/www/meridianlabs-inspect-scout-viewer-0.0.1.tgz deleted file mode 100644 index 0ce2ea921..000000000 Binary files a/src/inspect_scout/_view/www/meridianlabs-inspect-scout-viewer-0.0.1.tgz and /dev/null differ diff --git a/src/inspect_scout/_view/www/package.json b/src/inspect_scout/_view/www/package.json deleted file mode 100644 index 91c0e3311..000000000 --- a/src/inspect_scout/_view/www/package.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "name": "@meridianlabs/inspect-scout-viewer", - "version": "0.0.1", - "description": "Inspect Scout viewer for evaluation logs.", - "license": "MIT", - "type": "module", - "main": "./lib/index.js", - "module": "./lib/index.js", - "types": "./lib/index.d.ts", - "files": [ - "lib", - "lib/styles", - "lib/assets" - ], - "repository": { - "type": "git", - "url": "https://github.com/meridianlabs-ai/inspect_scout", - "directory": "src/inspect_scout/_view/www" - }, - "packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc", - "engines": { - "node": "22.21.1", - "pnpm": ">=9.0.0" - }, - "scripts": { - "types:generate": "node scripts/generate-types.js", - "prebuild": "pnpm types:generate", - "build": "npm run build:app", - "build:lib": "vite build --mode library", - "build:app": "vite build --mode development", - "watch": "vite build --watch", - "watch:dev": "NODE_ENV=development vite build --watch --mode development", - "dev": "vite", - "prepublishOnly": "npm version from-git --no-git-tag-version --allow-same-version && pnpm build:lib", - "preview": "vite preview", - "lint": "eslint . --max-warnings 0", - "lint:fix": "eslint . --fix --max-warnings 0", - "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", - "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", - "typecheck": "tsc --noEmit", - "check": "pnpm lint:fix && pnpm format && pnpm typecheck", - "test": "vitest run", - "test:watch": "vitest", - "e2e": "playwright test --config playwright.config.ts", - "e2e:ui": "playwright test --config playwright.config.ts --ui", - "e2e:headed": "playwright test --config playwright.config.ts --headed" - }, - "exports": { - ".": { - "import": "./lib/index.js", - "types": "./lib/index.d.ts" - }, - "./styles/index.css": "./lib/styles/index.css" - }, - "peerDependencies": { - "@tanstack/react-query": "^5.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@eslint/js": "^9.39.2", - "@msw/playwright": "^0.4.5", - "@playwright/test": "^1.58.1", - "@tanstack/react-query": "^5.90.12", - "@tanstack/react-query-devtools": "^5.91.1", - "@testing-library/react": "^16.3.1", - "@types/bootstrap": "^5.2.10", - "@types/css-modules": "^1.0.5", - "@types/eslint__js": "^9.14.0", - "@types/json5": "^2.2.0", - "@types/lodash-es": "^4.17.12", - "@types/lz4js": "^0.2.2", - "@types/markdown-it": "^14.1.2", - "@types/prismjs": "^1.26.5", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.4", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.8", - "eslint-import-resolver-typescript": "^4.4.4", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.26", - "eventsource": "^4.1.0", - "jsdom": "^27.3.0", - "msw": "^2.12.8", - "openapi-typescript": "^7.10.1", - "postcss-url": "^10.1.3", - "prettier": "^3.7.4", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "react-router-dom": "^7.9.4", - "typed-css-modules": "^0.9.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.50.0", - "typescript-plugin-css-modules": "^5.2.0", - "vite": "^7.3.0", - "vite-plugin-dts": "^4.5.4", - "vitest": "^4.0.16", - "zstd-codec": "^0.1.5" - }, - "dependencies": { - "@popperjs/core": "^2.11.8", - "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.12", - "@vscode-elements/react-elements": "^2.4.0", - "ag-grid-community": "^34.3.1", - "ag-grid-react": "^34.3.1", - "ansi-output": "^0.0.9", - "apache-arrow": "^21.1.0", - "arquero": "^8.0.3", - "asciinema-player": "^3.13.5", - "bootstrap": "^5.3.8", - "bootstrap-icons": "^1.13.1", - "clsx": "^2.1.1", - "fzstd": "^0.1.1", - "immer": "^10.2.0", - "json5": "^2.2.3", - "jsondiffpatch": "^0.7.3", - "lodash-es": "^4.17.23", - "lz4js": "^0.2.0", - "markdown-it": "^14.1.0", - "markdown-it-mathjax3": "^5.2.0", - "mathjax-full": "^3.2.2", - "prismjs": "^1.30.0", - "react-popper": "^2.3.0", - "react-router-dom": "^7.11.0", - "react-virtuoso": "^4.17.0", - "zustand": "^5.0.9" - }, - "pnpm": { - "overrides": { - "yaml": "^2.4.2" - } - } -} diff --git a/src/inspect_scout/_view/www/playwright.config.ts b/src/inspect_scout/_view/www/playwright.config.ts deleted file mode 100644 index 1cf677a6f..000000000 --- a/src/inspect_scout/_view/www/playwright.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; - -const baseURL = "http://localhost:5173"; - -export default defineConfig({ - testDir: "./e2e", - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: "html", - use: { - baseURL, - trace: "on-first-retry", - screenshot: "only-on-failure", - }, - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - ], - webServer: { - command: "pnpm dev", - url: baseURL, - reuseExistingServer: !process.env.CI, - }, -}); diff --git a/src/inspect_scout/_view/www/pnpm-lock.yaml b/src/inspect_scout/_view/www/pnpm-lock.yaml deleted file mode 100644 index ccee447eb..000000000 --- a/src/inspect_scout/_view/www/pnpm-lock.yaml +++ /dev/null @@ -1,7505 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -overrides: - yaml: ^2.4.2 - -importers: - - .: - dependencies: - '@popperjs/core': - specifier: ^2.11.8 - version: 2.11.8 - '@tanstack/react-table': - specifier: ^8.21.3 - version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-virtual': - specifier: ^3.13.12 - version: 3.13.12(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@vscode-elements/react-elements': - specifier: ^2.4.0 - version: 2.4.0(@types/react@19.2.7)(@vscode/codicons@0.0.44)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - ag-grid-community: - specifier: ^34.3.1 - version: 34.3.1 - ag-grid-react: - specifier: ^34.3.1 - version: 34.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - ansi-output: - specifier: ^0.0.9 - version: 0.0.9 - apache-arrow: - specifier: ^21.1.0 - version: 21.1.0 - arquero: - specifier: ^8.0.3 - version: 8.0.3 - asciinema-player: - specifier: ^3.13.5 - version: 3.13.5 - bootstrap: - specifier: ^5.3.8 - version: 5.3.8(@popperjs/core@2.11.8) - bootstrap-icons: - specifier: ^1.13.1 - version: 1.13.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 - fzstd: - specifier: ^0.1.1 - version: 0.1.1 - immer: - specifier: ^10.2.0 - version: 10.2.0 - json5: - specifier: ^2.2.3 - version: 2.2.3 - jsondiffpatch: - specifier: ^0.7.3 - version: 0.7.3 - lodash-es: - specifier: ^4.17.23 - version: 4.17.23 - lz4js: - specifier: ^0.2.0 - version: 0.2.0 - markdown-it: - specifier: ^14.1.0 - version: 14.1.0 - markdown-it-mathjax3: - specifier: ^5.2.0 - version: 5.2.0 - mathjax-full: - specifier: ^3.2.2 - version: 3.2.2 - prismjs: - specifier: ^1.30.0 - version: 1.30.0 - react-popper: - specifier: ^2.3.0 - version: 2.3.0(@popperjs/core@2.11.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react-router-dom: - specifier: ^7.11.0 - version: 7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react-virtuoso: - specifier: ^4.17.0 - version: 4.17.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - zustand: - specifier: ^5.0.9 - version: 5.0.9(@types/react@19.2.7)(immer@10.2.0)(react@19.2.3) - devDependencies: - '@eslint/js': - specifier: ^9.39.2 - version: 9.39.2 - '@msw/playwright': - specifier: ^0.4.5 - version: 0.4.5(msw@2.12.8(@types/node@24.10.1)(typescript@5.9.3)) - '@playwright/test': - specifier: ^1.58.1 - version: 1.58.1 - '@tanstack/react-query': - specifier: ^5.90.12 - version: 5.90.12(react@19.2.3) - '@tanstack/react-query-devtools': - specifier: ^5.91.1 - version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3) - '@testing-library/react': - specifier: ^16.3.1 - version: 16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@types/bootstrap': - specifier: ^5.2.10 - version: 5.2.10 - '@types/css-modules': - specifier: ^1.0.5 - version: 1.0.5 - '@types/eslint__js': - specifier: ^9.14.0 - version: 9.14.0 - '@types/json5': - specifier: ^2.2.0 - version: 2.2.0 - '@types/lodash-es': - specifier: ^4.17.12 - version: 4.17.12 - '@types/lz4js': - specifier: ^0.2.2 - version: 0.2.2 - '@types/markdown-it': - specifier: ^14.1.2 - version: 14.1.2 - '@types/prismjs': - specifier: ^1.26.5 - version: 1.26.5 - '@types/react': - specifier: ^19.2.7 - version: 19.2.7 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.7) - '@vitejs/plugin-react': - specifier: ^5.1.4 - version: 5.1.4(vite@7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1)) - eslint: - specifier: ^9.39.2 - version: 9.39.2 - eslint-config-prettier: - specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2) - eslint-import-resolver-typescript: - specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2) - eslint-plugin-import: - specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) - eslint-plugin-react: - specifier: ^7.37.5 - version: 7.37.5(eslint@9.39.2) - eslint-plugin-react-hooks: - specifier: ^7.0.1 - version: 7.0.1(eslint@9.39.2) - eslint-plugin-react-refresh: - specifier: ^0.4.26 - version: 0.4.26(eslint@9.39.2) - eventsource: - specifier: ^4.1.0 - version: 4.1.0 - jsdom: - specifier: ^27.3.0 - version: 27.3.0 - msw: - specifier: ^2.12.8 - version: 2.12.8(@types/node@24.10.1)(typescript@5.9.3) - openapi-typescript: - specifier: ^7.10.1 - version: 7.10.1(typescript@5.9.3) - postcss-url: - specifier: ^10.1.3 - version: 10.1.3(postcss@8.5.6) - prettier: - specifier: ^3.7.4 - version: 3.7.4 - react: - specifier: ^19.2.3 - version: 19.2.3 - react-dom: - specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) - typed-css-modules: - specifier: ^0.9.1 - version: 0.9.1 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - typescript-eslint: - specifier: ^8.50.0 - version: 8.50.0(eslint@9.39.2)(typescript@5.9.3) - typescript-plugin-css-modules: - specifier: ^5.2.0 - version: 5.2.0(typescript@5.9.3) - vite: - specifier: ^7.3.0 - version: 7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1) - vite-plugin-dts: - specifier: ^4.5.4 - version: 4.5.4(@types/node@24.10.1)(rollup@4.52.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1)) - vitest: - specifier: ^4.0.16 - version: 4.0.16(@types/node@24.10.1)(jsdom@27.3.0)(less@4.4.2)(msw@2.12.8(@types/node@24.10.1)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1) - zstd-codec: - specifier: ^0.1.5 - version: 0.1.5 - -packages: - - '@acemir/cssom@0.9.29': - resolution: {integrity: sha512-G90x0VW+9nW4dFajtjCoT+NM0scAfH9Mb08IcjgFHYbfiL/lU04dTF9JuVOi3/OH+DJCQdcIseSXkdCB9Ky6JA==} - - '@adobe/css-tools@4.3.3': - resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} - - '@asamuzakjp/css-color@4.1.1': - resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==} - - '@asamuzakjp/dom-selector@6.7.6': - resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==} - - '@asamuzakjp/nwsapi@2.3.9': - resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.28.5': - resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.28.5': - resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.28.5': - resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.3': - resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.28.4': - resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.28.6': - resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/runtime@7.28.4': - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} - engines: {node: '>=6.9.0'} - - '@babel/runtime@7.28.6': - resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.28.5': - resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - - '@csstools/color-helpers@5.1.0': - resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} - engines: {node: '>=18'} - - '@csstools/css-calc@2.1.4': - resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} - engines: {node: '>=18'} - peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 - - '@csstools/css-color-parser@3.1.0': - resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} - engines: {node: '>=18'} - peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 - - '@csstools/css-parser-algorithms@3.0.5': - resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} - engines: {node: '>=18'} - peerDependencies: - '@csstools/css-tokenizer': ^3.0.4 - - '@csstools/css-syntax-patches-for-csstree@1.0.21': - resolution: {integrity: sha512-plP8N8zKfEZ26figX4Nvajx8DuzfuRpLTqglQ5d0chfnt35Qt3X+m6ASZ+rG0D0kxe/upDVNwSIVJP5n4FuNfw==} - engines: {node: '>=18'} - - '@csstools/css-tokenizer@3.0.4': - resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} - engines: {node: '>=18'} - - '@dmsnell/diff-match-patch@1.1.0': - resolution: {integrity: sha512-yejLPmM5pjsGvxS9gXablUSbInW7H976c/FJ4iQxWIm7/38xBySRemTPDe34lhg1gVLbJntX0+sH0jYfU+PN9A==} - - '@emnapi/core@1.7.0': - resolution: {integrity: sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==} - - '@emnapi/runtime@1.7.0': - resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==} - - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - - '@esbuild/aix-ppc64@0.27.2': - resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.2': - resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.2': - resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.2': - resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.2': - resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.2': - resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.2': - resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.2': - resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.2': - resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.2': - resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.2': - resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.2': - resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.2': - resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.2': - resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.2': - resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.2': - resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.2': - resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.2': - resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.2': - resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.2': - resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.2': - resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.2': - resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.2': - resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.2': - resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.2': - resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.2': - resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - - '@inquirer/ansi@1.0.2': - resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} - engines: {node: '>=18'} - - '@inquirer/confirm@5.1.21': - resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/core@10.3.2': - resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/figures@1.0.15': - resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} - engines: {node: '>=18'} - - '@inquirer/type@3.0.10': - resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@lit-labs/ssr-dom-shim@1.5.1': - resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==} - - '@lit/context@1.1.6': - resolution: {integrity: sha512-M26qDE6UkQbZA2mQ3RjJ3Gzd8TxP+/0obMgE5HfkfLhEEyYE3Bui4A5XHiGPjy0MUGAyxB3QgVuw2ciS0kHn6A==} - - '@lit/react@1.0.8': - resolution: {integrity: sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw==} - peerDependencies: - '@types/react': 17 || 18 || 19 - - '@lit/reactive-element@2.1.2': - resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} - - '@microsoft/api-extractor-model@7.32.0': - resolution: {integrity: sha512-QIVJSreb8fGGJy1Qx0yzGVXxvHJN1WXgkFNHFheVv1iBJNqgvp+xeT3ienJmRwXmPPc5Es/cxBrXtKZJR3i7iw==} - - '@microsoft/api-extractor@7.55.0': - resolution: {integrity: sha512-TYc5OtAK/9E3HGgd2bIfSjQDYIwPc0dysf9rPiwXZGsq916I6W2oww9bhm1OxPOeg6rMfOX3PoroGd7oCryYog==} - hasBin: true - - '@microsoft/tsdoc-config@0.18.0': - resolution: {integrity: sha512-8N/vClYyfOH+l4fLkkr9+myAoR6M7akc8ntBJ4DJdWH2b09uVfr71+LTMpNyG19fNqWDg8KEDZhx5wxuqHyGjw==} - - '@microsoft/tsdoc@0.16.0': - resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} - - '@msw/playwright@0.4.5': - resolution: {integrity: sha512-t6tft9LrAmq8busJkzFcWhpPLGuk3h8tEXp5GGv7xhFpiB6xiRo5CgCatZ9x+e1kjcU2PgYi077B4qJ4E9vp4g==} - engines: {node: '>=20.0.0'} - peerDependencies: - msw: ^2.10.3 - - '@mswjs/interceptors@0.40.0': - resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} - engines: {node: '>=18'} - - '@mswjs/interceptors@0.41.0': - resolution: {integrity: sha512-edAo9bW53BLYeSK+UPRr2Iz1Fj9DeGMjytvVM0HXRoo750ElWUgPsZPAOTQa12EUiwgDErH2PsFNTLvk1jBxjQ==} - engines: {node: '>=18'} - - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - - '@open-draft/deferred-promise@2.2.0': - resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} - - '@open-draft/logger@0.3.0': - resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} - - '@open-draft/until@2.1.0': - resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - - '@parcel/watcher-android-arm64@2.5.1': - resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.1': - resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.1': - resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.1': - resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.1': - resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm-musl@2.5.1': - resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-arm64-glibc@2.5.1': - resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm64-musl@2.5.1': - resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-x64-glibc@2.5.1': - resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-x64-musl@2.5.1': - resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@parcel/watcher-win32-arm64@2.5.1': - resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.1': - resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.1': - resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.1': - resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} - engines: {node: '>= 10.0.0'} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@playwright/test@1.58.1': - resolution: {integrity: sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==} - engines: {node: '>=18'} - hasBin: true - - '@popperjs/core@2.11.8': - resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - - '@redocly/ajv@8.17.1': - resolution: {integrity: sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==} - - '@redocly/config@0.22.2': - resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} - - '@redocly/openapi-core@1.34.6': - resolution: {integrity: sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==} - engines: {node: '>=18.17.0', npm: '>=9.5.0'} - - '@rolldown/pluginutils@1.0.0-rc.3': - resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} - - '@rollup/pluginutils@5.3.0': - resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.52.5': - resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.52.5': - resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.52.5': - resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.52.5': - resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.52.5': - resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.52.5': - resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.52.5': - resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.52.5': - resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.52.5': - resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.52.5': - resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.52.5': - resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-gnu@4.52.5': - resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-gnu@4.52.5': - resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.52.5': - resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.52.5': - resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.52.5': - resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.52.5': - resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openharmony-arm64@4.52.5': - resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.52.5': - resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.52.5': - resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.52.5': - resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.52.5': - resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} - cpu: [x64] - os: [win32] - - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - - '@rushstack/node-core-library@5.18.0': - resolution: {integrity: sha512-XDebtBdw5S3SuZIt+Ra2NieT8kQ3D2Ow1HxhDQ/2soinswnOu9e7S69VSwTOLlQnx5mpWbONu+5JJjDxMAb6Fw==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - - '@rushstack/problem-matcher@0.1.1': - resolution: {integrity: sha512-Fm5XtS7+G8HLcJHCWpES5VmeMyjAKaWeyZU5qPzZC+22mPlJzAsOxymHiWIfuirtPckX3aptWws+K2d0BzniJA==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - - '@rushstack/rig-package@0.6.0': - resolution: {integrity: sha512-ZQmfzsLE2+Y91GF15c65L/slMRVhF6Hycq04D4TwtdGaUAbIXXg9c5pKA5KFU7M4QMaihoobp9JJYpYcaY3zOw==} - - '@rushstack/terminal@0.19.3': - resolution: {integrity: sha512-0P8G18gK9STyO+CNBvkKPnWGMxESxecTYqOcikHOVIHXa9uAuTK+Fw8TJq2Gng1w7W6wTC9uPX6hGNvrMll2wA==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - - '@rushstack/ts-command-line@5.1.3': - resolution: {integrity: sha512-Kdv0k/BnnxIYFlMVC1IxrIS0oGQd4T4b7vKfx52Y2+wk2WZSDFIvedr7JrhenzSlm3ou5KwtoTGTGd5nbODRug==} - - '@se-oss/deasync-darwin-arm64@1.0.1': - resolution: {integrity: sha512-0YWmIDEGQfW3GGopmZHhfA6mamsG0HFKZhmBzHVyFiMKkJts8kpQwGbGrWlK8eOAoPCihOsG6tCotYR3p7HZaQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@se-oss/deasync-darwin-x64@1.0.1': - resolution: {integrity: sha512-r3FRTLIXqGqOb1DjTLW3YhO/Dd1vA2qRLP0Ym3Wmk3yMv6c/nm15zg6UVoXbgBu8cjbvcsI/OfbHPdErmjMWsw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@se-oss/deasync-linux-arm64-gnu@1.0.1': - resolution: {integrity: sha512-657uRew7fZAx663Li03ilLV2lN09Dqb/NxawlDu8kKmboK1BLitHJRS+taiT5oFZqyIDrU45tlQKfCrW0p0sYA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@se-oss/deasync-linux-arm64-musl@1.0.1': - resolution: {integrity: sha512-IE3fIQPIJtko4lx9sRam+Zz0P4xbpAPJgDCHaz6k9cP1yUvVI179B4IZRnFx0GyjyQpm0KhHoIGHJc4KUmA81Q==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@se-oss/deasync-linux-x64-gnu@1.0.1': - resolution: {integrity: sha512-XQl7etZESGIjIraCyxfAey8ZTIJUB4dUFU3rPR/xLVn9bKpZGlJLIms0z3hoHX9mipO+Cqo53vK4IVm6A7U/ww==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@se-oss/deasync-linux-x64-musl@1.0.1': - resolution: {integrity: sha512-vWgFAZlqImqMV6jhCWV7C9wcCS1eb1ajhlKduBRPfyUxxkoObe+EqTG2BKJAuafxp3/KS1aUsIMJma9mhwFvow==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@se-oss/deasync-win32-arm64-msvc@1.0.1': - resolution: {integrity: sha512-yk7lEE7Zd8GX7o6CuUbg3HnnmUhBx4tgfn5ff3eoq05CgBO6Z3ZtL4l+utAe1cxcFaXPhyvcgnHYyA4OF544tg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@se-oss/deasync-win32-x64-msvc@1.0.1': - resolution: {integrity: sha512-ixizmuLGRPGyAesWUNWVzVOsvuunNb/qMqU8SmjfLR/vVgzdQEkSHFf+fkX9GXPN6FDv+DAz5uskTzhjUyCXFA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@se-oss/deasync@1.0.1': - resolution: {integrity: sha512-Ha7P/xCNxOuH72BNdLRWs4TT8rsMMrERnHtfKWBeTWu+UFW9OBTrRgfZJOlbAAQFR0l4Q30cpAn8CuR7PXWcPg==} - engines: {node: '>= 10'} - - '@solid-primitives/refs@1.1.2': - resolution: {integrity: sha512-K7tf2thy7L+YJjdqXspXOg5xvNEOH8tgEWsp0+1mQk3obHBRD6hEjYZk7p7FlJphSZImS35je3UfmWuD7MhDfg==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/transition-group@1.1.2': - resolution: {integrity: sha512-gnHS0OmcdjeoHN9n7Khu8KNrOlRc8a2weETDt2YT6o1zeW/XtUC6Db3Q9pkMU/9cCKdEmN4b0a/41MKAHRhzWA==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/utils@6.3.2': - resolution: {integrity: sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==} - peerDependencies: - solid-js: ^1.6.12 - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@swc/helpers@0.5.17': - resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - - '@tanstack/query-core@5.90.12': - resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} - - '@tanstack/query-devtools@5.91.1': - resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==} - - '@tanstack/react-query-devtools@5.91.1': - resolution: {integrity: sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==} - peerDependencies: - '@tanstack/react-query': ^5.90.10 - react: ^18 || ^19 - - '@tanstack/react-query@5.90.12': - resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==} - peerDependencies: - react: ^18 || ^19 - - '@tanstack/react-table@8.21.3': - resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} - engines: {node: '>=12'} - peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' - - '@tanstack/react-virtual@3.13.12': - resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - '@tanstack/table-core@8.21.3': - resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} - engines: {node: '>=12'} - - '@tanstack/virtual-core@3.13.12': - resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} - - '@testing-library/dom@10.4.1': - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} - engines: {node: '>=18'} - - '@testing-library/react@16.3.1': - resolution: {integrity: sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==} - engines: {node: '>=18'} - peerDependencies: - '@testing-library/dom': ^10.0.0 - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - - '@types/argparse@1.0.38': - resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} - - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - - '@types/bootstrap@5.2.10': - resolution: {integrity: sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==} - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/command-line-args@5.2.3': - resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} - - '@types/command-line-usage@5.0.4': - resolution: {integrity: sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==} - - '@types/css-modules@1.0.5': - resolution: {integrity: sha512-oeKafs/df9lwOvtfiXVliZsocFVOexK9PZtLQWuPeuVCFR7jwiqlg60lu80JTe5NFNtH3tnV6Fs/ySR8BUPHAw==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/eslint__js@9.14.0': - resolution: {integrity: sha512-s0jepCjOJWB/GKcuba4jISaVpBudw3ClXJ3fUK4tugChUMQsp6kSwuA8Dcx6wFd/JsJqcY8n4rEpa5RTHs5ypA==} - deprecated: This is a stub types definition. @eslint/js provides its own type definitions, so you do not need this installed. - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - - '@types/json5@2.2.0': - resolution: {integrity: sha512-NrVug5woqbvNZ0WX+Gv4R+L4TGddtmFek2u8RtccAgFZWtS9QXF2xCXY22/M4nzkaKF0q9Fc6M/5rxLDhfwc/A==} - deprecated: This is a stub types definition. json5 provides its own type definitions, so you do not need this installed. - - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - - '@types/lodash-es@4.17.12': - resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - - '@types/lodash@4.17.23': - resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} - - '@types/lz4js@0.2.2': - resolution: {integrity: sha512-pqXfoJ2AwllcLTf1Nia0+HT1KHj8z4wWo3bBP6vn7Aen5ySywBqrh/u1TyBwWxuKDS+mPzVHkPbHivqZEfk6pA==} - - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - - '@types/node@24.10.1': - resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} - - '@types/postcss-modules-local-by-default@4.0.2': - resolution: {integrity: sha512-CtYCcD+L+trB3reJPny+bKWKMzPfxEyQpKIwit7kErnOexf5/faaGpkFy4I5AwbV4hp1sk7/aTg0tt0B67VkLQ==} - - '@types/postcss-modules-scope@3.0.4': - resolution: {integrity: sha512-//ygSisVq9kVI0sqx3UPLzWIMCmtSVrzdljtuaAEJtGoGnpjBikZ2sXO5MpH9SnWX9HRfXxHifDAXcQjupWnIQ==} - - '@types/prismjs@1.26.5': - resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.7': - resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} - - '@types/statuses@2.0.6': - resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} - - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - - '@typescript-eslint/eslint-plugin@8.50.0': - resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.50.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/parser@8.50.0': - resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.50.0': - resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/scope-manager@8.50.0': - resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.50.0': - resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/type-utils@8.50.0': - resolution: {integrity: sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/types@8.50.0': - resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.50.0': - resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.50.0': - resolution: {integrity: sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.50.0': - resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} - cpu: [arm] - os: [android] - - '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} - cpu: [arm64] - os: [android] - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} - cpu: [arm64] - os: [darwin] - - '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} - cpu: [x64] - os: [darwin] - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} - cpu: [x64] - os: [freebsd] - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} - cpu: [arm64] - os: [win32] - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} - cpu: [ia32] - os: [win32] - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} - cpu: [x64] - os: [win32] - - '@uwdata/flechette@2.2.6': - resolution: {integrity: sha512-DoeWHYWHvda7kaxa1BeKVp0+S9ZiqGoRxLC+z9uuYyfMLDQ+3vW4s4WVxnQYFJm8nshS5ezAsbKBN9ojCBtdEQ==} - - '@vitejs/plugin-react@5.1.4': - resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - - '@vitest/expect@4.0.16': - resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==} - - '@vitest/mocker@4.0.16': - resolution: {integrity: sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.0.16': - resolution: {integrity: sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==} - - '@vitest/runner@4.0.16': - resolution: {integrity: sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==} - - '@vitest/snapshot@4.0.16': - resolution: {integrity: sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==} - - '@vitest/spy@4.0.16': - resolution: {integrity: sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==} - - '@vitest/utils@4.0.16': - resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} - - '@volar/language-core@2.4.23': - resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} - - '@volar/source-map@2.4.23': - resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} - - '@volar/typescript@2.4.23': - resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} - - '@vscode-elements/elements@2.4.0': - resolution: {integrity: sha512-3VzHabhhT+mdaCTQhR0uxN/BFHcy+QjRVxnqjoG/ERsRxNEYZ+BycMFwmvV1H2gZSW9GBpUS6YpmPfAIhLAmgg==} - peerDependencies: - '@vscode/codicons': '>=0.0.40' - - '@vscode-elements/react-elements@2.4.0': - resolution: {integrity: sha512-gDLHE+JE0ViYN+Bzp0obUKBA+guc8Vk0X3n1157z9J+X9GCwHo5ZNziDmiNXyd3QA4IE/kkcg5+3RsemYLlZqg==} - peerDependencies: - react: 17 || 18 || 19 - react-dom: 17 || 18 || 19 - - '@vscode/codicons@0.0.44': - resolution: {integrity: sha512-F7qPRumUK3EHjNdopfICLGRf3iNPoZQt+McTHAn4AlOWPB3W2kL4H0S7uqEqbyZ6rCxaeDjpAn3MCUnwTu/VJQ==} - - '@vue/compiler-core@3.5.24': - resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==} - - '@vue/compiler-dom@3.5.24': - resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==} - - '@vue/compiler-vue2@2.7.16': - resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} - - '@vue/language-core@2.2.0': - resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@vue/shared@3.5.24': - resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==} - - '@xmldom/xmldom@0.9.8': - resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==} - engines: {node: '>=14.6'} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ag-charts-types@12.3.1: - resolution: {integrity: sha512-5216xYoawnvMXDFI6kTpPku+mH0Csiwu/FE7lsAm8Z22HEN6ciSG/V7g+IrpLWncELqksgENebCTP75PZ3CsHA==} - - ag-grid-community@34.3.1: - resolution: {integrity: sha512-PwlrPudsFOzGumphi2y9ihWeaUlIwKhOra/MXu2LjeV2U8DgLLcYS8CartE5Hszhn1poJHawwI9HWrxlKliwdw==} - - ag-grid-react@34.3.1: - resolution: {integrity: sha512-1UTlBT+xJkjNZAuf7RxK61mgxKGTPB+6XR99oIHq7cYC89kJmLbWqhHt/1XqRWF5cAgSKk8u+HtOQaN8tAZStw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - - ajv-draft-04@1.0.0: - resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} - peerDependencies: - ajv: ^8.5.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} - - ajv@8.13.0: - resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} - - alien-signals@0.4.14: - resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} - - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - - ansi-output@0.0.9: - resolution: {integrity: sha512-6kLL1/P4hukih+MU2U0faECoH4F2gGDQy00gjCAaW9ojj6voOdlHtFtmZxYC0HFAtPxzUFt/etZAZLV2GaXWoA==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - apache-arrow@21.1.0: - resolution: {integrity: sha512-kQrYLxhC+NTVVZ4CCzGF6L/uPVOzJmD1T3XgbiUnP7oTeVFOFgEUu6IKNwCDkpFoBVqDKQivlX4RUFqqnWFlEA==} - hasBin: true - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - - arquero@8.0.3: - resolution: {integrity: sha512-7YQwe/GPVBUiahaPwEwgvu6VHyuhX0Ut61JZlIJYsAobOH5unLBwTmh43BobhX/N4dW1sb8WyKlQ8GjFq2whzQ==} - - array-back@6.2.2: - resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} - engines: {node: '>=12.17'} - - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} - - array.prototype.findlast@1.2.5: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - engines: {node: '>= 0.4'} - - array.prototype.findlastindex@1.2.6: - resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} - engines: {node: '>= 0.4'} - - array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - array.prototype.tosorted@1.1.4: - resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - - asciinema-player@3.13.5: - resolution: {integrity: sha512-mgpJc9g6I+k4Tz5qVUNd0H+GoYlhiUwvlay6vD6IXiuiWOWhBOjxbvqQ1bcI/HPTrOYxhTyxZuzHIXM36Tw60Q==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - baseline-browser-mapping@2.9.19: - resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} - hasBin: true - - bidi-js@1.0.3: - resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - bootstrap-icons@1.13.1: - resolution: {integrity: sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==} - - bootstrap@5.3.8: - resolution: {integrity: sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==} - peerDependencies: - '@popperjs/core': ^2.11.8 - - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-lite@1.0.30001769: - resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} - - chai@6.2.1: - resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} - engines: {node: '>=18'} - - chalk-template@0.4.0: - resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} - engines: {node: '>=12'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - change-case@5.4.4: - resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - cli-width@4.1.0: - resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} - engines: {node: '>= 12'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - colorette@1.4.0: - resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} - - command-line-args@6.0.1: - resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==} - engines: {node: '>=12.20'} - peerDependencies: - '@75lb/nature': latest - peerDependenciesMeta: - '@75lb/nature': - optional: true - - command-line-usage@7.0.3: - resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==} - engines: {node: '>=12.20.0'} - - commander@13.1.0: - resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} - engines: {node: '>=18'} - - compare-versions@6.1.1: - resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - confbox@0.2.2: - resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cookie@1.0.2: - resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} - engines: {node: '>=18'} - - copy-anything@2.0.6: - resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - css-tree@3.1.0: - resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - cssstyle@5.3.5: - resolution: {integrity: sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag==} - engines: {node: '>=20'} - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - cuint@0.2.2: - resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==} - - data-urls@6.0.0: - resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} - engines: {node: '>=20'} - - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - - de-indent@1.0.2: - resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} - - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - detect-libc@1.0.3: - resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} - engines: {node: '>=0.10'} - hasBin: true - - diff@8.0.2: - resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} - engines: {node: '>=0.3.1'} - - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - electron-to-chromium@1.5.286: - resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - - errno@0.1.8: - resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} - hasBin: true - - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} - engines: {node: '>= 0.4'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-iterator-helpers@1.2.1: - resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} - engines: {node: '>= 0.4'} - - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} - engines: {node: '>= 0.4'} - - esbuild@0.27.2: - resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-config-prettier@10.1.8: - resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-import-context@0.1.9: - resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - peerDependencies: - unrs-resolver: ^1.0.0 - peerDependenciesMeta: - unrs-resolver: - optional: true - - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-import-resolver-typescript@4.4.4: - resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} - engines: {node: ^16.17.0 || >=18.6.0} - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - eslint-plugin-import-x: '*' - peerDependenciesMeta: - eslint-plugin-import: - optional: true - eslint-plugin-import-x: - optional: true - - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-import@2.32.0: - resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - - eslint-plugin-react-hooks@7.0.1: - resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} - engines: {node: '>=18'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - - eslint-plugin-react-refresh@0.4.26: - resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} - peerDependencies: - eslint: '>=8.40' - - eslint-plugin-react@7.37.5: - resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - esm@3.2.25: - resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} - engines: {node: '>=6'} - - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - eventsource-parser@3.0.6: - resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} - engines: {node: '>=18.0.0'} - - eventsource@4.1.0: - resolution: {integrity: sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ==} - engines: {node: '>=20.0.0'} - - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} - - exsolve@1.0.8: - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-replace@5.0.2: - resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==} - engines: {node: '>=14'} - peerDependencies: - '@75lb/nature': latest - peerDependenciesMeta: - '@75lb/nature': - optional: true - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatbuffers@25.9.23: - resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - - fs-extra@11.3.2: - resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} - engines: {node: '>=14.14'} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} - engines: {node: '>= 0.4'} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - fzstd@0.1.1: - resolution: {integrity: sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA==} - - generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - - get-tsconfig@4.13.0: - resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - hasBin: true - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - graphql@16.12.0: - resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} - engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - - headers-polyfill@4.0.3: - resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} - - hermes-estree@0.25.1: - resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} - - hermes-parser@0.25.1: - resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - - html-encoding-sniffer@4.0.0: - resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} - engines: {node: '>=18'} - - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - icss-replace-symbols@1.1.0: - resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} - - icss-utils@5.1.0: - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - image-size@0.5.5: - resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} - engines: {node: '>=0.10.0'} - hasBin: true - - immer@10.2.0: - resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} - - immutable@5.1.4: - resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - import-lazy@4.0.0: - resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} - engines: {node: '>=8'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - index-to-position@1.2.0: - resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} - engines: {node: '>=18'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - - is-bun-module@2.0.0: - resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} - engines: {node: '>= 0.4'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - - is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - - is-node-process@1.2.0: - resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-there@4.5.2: - resolution: {integrity: sha512-ixMkfz3rtS1vEsLf0TjgjqUn96Q0ukpUVDMnPYVocJyTzu2G/QgEtqYddcHZawHO+R31cKVPggJmBLrm1vJCOg==} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - - is-what@3.14.1: - resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - iterator.prototype@1.1.5: - resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} - engines: {node: '>= 0.4'} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - - jju@1.4.0: - resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - - js-levenshtein@1.1.6: - resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} - engines: {node: '>=0.10.0'} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsdom@27.3.0: - resolution: {integrity: sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - peerDependencies: - canvas: ^3.0.0 - peerDependenciesMeta: - canvas: - optional: true - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-bignum@0.0.3: - resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} - engines: {node: '>=0.8'} - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jsondiffpatch@0.7.3: - resolution: {integrity: sha512-zd4dqFiXSYyant2WgSXAZ9+yYqilNVvragVNkNRn2IFZKgjyULNrKRznqN4Zon0MkLueCg+3QaPVCnDAVP20OQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - - jsonfile@6.2.0: - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - - jsx-ast-utils@3.3.5: - resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} - engines: {node: '>=4.0'} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - kolorist@1.8.0: - resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - - less@4.4.2: - resolution: {integrity: sha512-j1n1IuTX1VQjIy3tT7cyGbX7nvQOsFLoIqobZv4ttI5axP923gA44zUj6miiA6R5Aoms4sEGVIIcucXUbRI14g==} - engines: {node: '>=14'} - hasBin: true - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - - lit-element@4.2.2: - resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} - - lit-html@3.3.2: - resolution: {integrity: sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==} - - lit@3.3.2: - resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==} - - local-pkg@1.1.2: - resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} - engines: {node: '>=14'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash-es@4.17.23: - resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} - - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} - engines: {node: 20 || >=22} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - - lz4js@0.2.0: - resolution: {integrity: sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - make-dir@2.1.0: - resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} - engines: {node: '>=6'} - - make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - - markdown-it-mathjax3@5.2.0: - resolution: {integrity: sha512-R+XAy5/7vSGuhG9Z0/cJm6zKxOzStcScfSKVwoarh4nBra+v1KClvbALr/xFTEe9iQhwfQM4SJnO68LXL+btMA==} - - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - mathjax-full@3.2.2: - resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} - deprecated: Version 4 replaces this package with the scoped package @mathjax/src - - mathxyjax3@0.8.3: - resolution: {integrity: sha512-eXjFaiyQsTdVOeTFoFaFJ/r1FITpB1f9c5MW4FETfcoVV/+xa5SD9pS05AwugzL/gNuDtWXrTOSmoD2e0Du+UA==} - - mdn-data@2.12.2: - resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} - - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - - mhchemparser@4.2.1: - resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - - mime@2.5.2: - resolution: {integrity: sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==} - engines: {node: '>=4.0.0'} - hasBin: true - - minimatch@10.0.3: - resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} - engines: {node: 20 || >=22} - - minimatch@3.0.8: - resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - mj-context-menu@0.6.1: - resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} - - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - - mlly@1.8.0: - resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - msw@2.12.8: - resolution: {integrity: sha512-KOriJUhjefCO+liF7Ie1KlSXcBAQEzuLhPZ4EKuEUSEmAR4YhuuzT9YuGxTipjqDrg6eWQ6oMoGVhvEnqukFGg==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - typescript: '>= 4.8.x' - peerDependenciesMeta: - typescript: - optional: true - - muggle-string@0.4.1: - resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - - mute-stream@2.0.0: - resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} - engines: {node: ^18.17.0 || >=20.5.0} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - napi-postinstall@0.3.4: - resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - needle@3.3.1: - resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==} - engines: {node: '>= 4.4.x'} - hasBin: true - - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - object.entries@1.1.9: - resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - openapi-typescript@7.10.1: - resolution: {integrity: sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==} - hasBin: true - peerDependencies: - typescript: ^5.x - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - outvariant@1.4.3: - resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-json@8.3.0: - resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} - engines: {node: '>=18'} - - parse-node-version@1.0.1: - resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} - engines: {node: '>= 0.10'} - - parse5@8.0.0: - resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} - - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - pkg-types@2.3.0: - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - - playwright-core@1.58.1: - resolution: {integrity: sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.58.1: - resolution: {integrity: sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==} - engines: {node: '>=18'} - hasBin: true - - pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - postcss-load-config@3.1.4: - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} - engines: {node: '>= 10'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - - postcss-modules-extract-imports@3.1.0: - resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-local-by-default@4.2.0: - resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-scope@3.2.1: - resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-values@4.0.0: - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-selector-parser@7.1.0: - resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} - engines: {node: '>=4'} - - postcss-url@10.1.3: - resolution: {integrity: sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==} - engines: {node: '>=10'} - peerDependencies: - postcss: ^8.0.0 - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier@3.7.4: - resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} - engines: {node: '>=14'} - hasBin: true - - pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - - prismjs@1.30.0: - resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} - engines: {node: '>=6'} - - prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - - prr@1.0.1: - resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} - - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - quansync@0.2.11: - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - - react-dom@19.2.3: - resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} - peerDependencies: - react: ^19.2.3 - - react-fast-compare@3.2.2: - resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - - react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - - react-popper@2.3.0: - resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} - peerDependencies: - '@popperjs/core': ^2.0.0 - react: ^16.8.0 || ^17 || ^18 - react-dom: ^16.8.0 || ^17 || ^18 - - react-refresh@0.18.0: - resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} - engines: {node: '>=0.10.0'} - - react-router-dom@7.11.0: - resolution: {integrity: sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: '>=18' - react-dom: '>=18' - - react-router@7.11.0: - resolution: {integrity: sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: '>=18' - react-dom: '>=18' - peerDependenciesMeta: - react-dom: - optional: true - - react-virtuoso@4.17.0: - resolution: {integrity: sha512-od3pi2v13v31uzn5zPXC2u3ouISFCVhjFVFch2VvS2Cx7pWA2F1aJa3XhNTN2F07M3lhfnMnsmGeH+7wZICr7w==} - peerDependencies: - react: '>=16 || >=17 || >= 18 || >= 19' - react-dom: '>=16 || >=17 || >= 18 || >=19' - - react@19.2.3: - resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} - engines: {node: '>=0.10.0'} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - reserved-words@0.1.2: - resolution: {integrity: sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true - - resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} - hasBin: true - - rettime@0.10.1: - resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} - - rollup@4.52.5: - resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} - engines: {node: '>=0.4'} - - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - sass@1.93.2: - resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==} - engines: {node: '>=14.0.0'} - hasBin: true - - sax@1.3.0: - resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} - - sax@1.4.1: - resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - - saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true - - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - - seroval-plugins@1.3.3: - resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==} - engines: {node: '>=10'} - peerDependencies: - seroval: ^1.0 - - seroval@1.3.2: - resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} - engines: {node: '>=10'} - - set-cookie-parser@2.7.2: - resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - solid-js@1.9.10: - resolution: {integrity: sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==} - - solid-transition-group@0.2.3: - resolution: {integrity: sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg==} - engines: {node: '>=18.0.0', pnpm: '>=8.6.0'} - peerDependencies: - solid-js: ^1.6.12 - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} - - speech-rule-engine@4.1.2: - resolution: {integrity: sha512-S6ji+flMEga+1QU79NDbwZ8Ivf0S/MpupQQiIC0rTpU/ZTKgcajijJJb1OcByBQDjrXCN1/DJtGz4ZJeBMPGJw==} - hasBin: true - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - stable-hash-x@0.2.0: - resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} - engines: {node: '>=12.0.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} - - strict-event-emitter@0.5.1: - resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} - - string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} - engines: {node: '>= 0.4'} - - string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} - - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} - engines: {node: '>=12'} - - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - stylus@0.62.0: - resolution: {integrity: sha512-v3YCf31atbwJQIMtPNX8hcQ+okD4NQaTuKGUWfII8eaqn+3otrbttGL1zSMZAAtiPsBztQnujVBugg/cXFUpyg==} - hasBin: true - - supports-color@10.2.2: - resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} - engines: {node: '>=18'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - - table-layout@4.1.1: - resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} - engines: {node: '>=12.17'} - - tagged-tag@1.0.0: - resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} - engines: {node: '>=20'} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} - engines: {node: '>=14.0.0'} - - tldts-core@7.0.19: - resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} - - tldts@7.0.19: - resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} - hasBin: true - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - tough-cookie@6.0.0: - resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} - engines: {node: '>=16'} - - tr46@6.0.0: - resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} - engines: {node: '>=20'} - - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - - tsconfig-paths@4.2.0: - resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} - engines: {node: '>=6'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} - - type-fest@5.4.3: - resolution: {integrity: sha512-AXSAQJu79WGc79/3e9/CR77I/KQgeY1AhNvcShIH4PTcGYyC4xv6H4R4AUOwkPS5799KlVDAu8zExeCrkGquiA==} - engines: {node: '>=20'} - - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} - - typed-css-modules@0.9.1: - resolution: {integrity: sha512-W2HWKncdKd+bLWsnuWB2EyuQBzZ7KJ9Byr/67KLiiyGegcN52rOveun9JR8yAvuL5IXunRMxt0eORMtAUj5bmA==} - engines: {node: '>=18.0.0'} - hasBin: true - - typescript-eslint@8.50.0: - resolution: {integrity: sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - typescript-plugin-css-modules@5.2.0: - resolution: {integrity: sha512-c5pAU5d+m3GciDr/WhkFldz1NIEGBafuP/3xhFt9BEXS2gmn/LvjkoZ11vEBIuP8LkXfPNhOt1BUhM5efFuwOw==} - peerDependencies: - typescript: '>=4.0.0' - - typescript@5.8.2: - resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} - engines: {node: '>=14.17'} - hasBin: true - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - typical@7.3.0: - resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} - engines: {node: '>=12.17'} - - uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - - ufo@1.6.1: - resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - - until-async@3.0.2: - resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} - - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - vite-plugin-dts@4.5.4: - resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} - peerDependencies: - typescript: '*' - vite: '*' - peerDependenciesMeta: - vite: - optional: true - - vite@7.3.0: - resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@4.0.16: - resolution: {integrity: sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.16 - '@vitest/browser-preview': 4.0.16 - '@vitest/browser-webdriverio': 4.0.16 - '@vitest/ui': 4.0.16 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - - w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} - - warning@4.0.3: - resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} - - webidl-conversions@8.0.0: - resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} - engines: {node: '>=20'} - - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - - whatwg-url@15.1.0: - resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} - engines: {node: '>=20'} - - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} - engines: {node: '>= 0.4'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - wicked-good-xpath@1.3.0: - resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - wordwrapjs@5.1.1: - resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} - engines: {node: '>=12.17'} - - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xml-name-validator@5.0.0: - resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} - engines: {node: '>=18'} - - xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - - xxhashjs@0.2.2: - resolution: {integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - - yaml-ast-parser@0.0.43: - resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} - - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} - engines: {node: '>= 14.6'} - hasBin: true - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - yoctocolors-cjs@2.1.3: - resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} - engines: {node: '>=18'} - - zod-validation-error@4.0.2: - resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - - zod@4.1.12: - resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} - - zstd-codec@0.1.5: - resolution: {integrity: sha512-v3fyjpK8S/dpY/X5WxqTK3IoCnp/ZOLxn144GZVlNUjtwAchzrVo03h+oMATFhCIiJ5KTr4V3vDQQYz4RU684g==} - - zustand@5.0.9: - resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} - engines: {node: '>=12.20.0'} - peerDependencies: - '@types/react': '>=18.0.0' - immer: '>=9.0.6' - react: '>=18.0.0' - use-sync-external-store: '>=1.2.0' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - use-sync-external-store: - optional: true - -snapshots: - - '@acemir/cssom@0.9.29': {} - - '@adobe/css-tools@4.3.3': - optional: true - - '@asamuzakjp/css-color@4.1.1': - dependencies: - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - lru-cache: 11.2.4 - - '@asamuzakjp/dom-selector@6.7.6': - dependencies: - '@asamuzakjp/nwsapi': 2.3.9 - bidi-js: 1.0.3 - css-tree: 3.1.0 - is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.4 - - '@asamuzakjp/nwsapi@2.3.9': {} - - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.28.5': {} - - '@babel/compat-data@7.29.0': {} - - '@babel/core@7.28.5': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) - '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@10.2.2) - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@10.2.2) - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.28.5': - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.27.2': - dependencies: - '@babel/compat-data': 7.28.5 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-compilation-targets@7.28.6': - dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.27.1': - dependencies: - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.27.1': {} - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.28.4': - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 - - '@babel/helpers@7.28.6': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - - '@babel/parser@7.28.5': - dependencies: - '@babel/types': 7.28.5 - - '@babel/parser@7.29.0': - dependencies: - '@babel/types': 7.29.0 - - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/runtime@7.28.4': {} - - '@babel/runtime@7.28.6': {} - - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - - '@babel/traverse@7.28.5': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - - '@babel/types@7.28.5': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@csstools/color-helpers@5.1.0': {} - - '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - - '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/color-helpers': 5.1.0 - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - - '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/css-tokenizer': 3.0.4 - - '@csstools/css-syntax-patches-for-csstree@1.0.21': {} - - '@csstools/css-tokenizer@3.0.4': {} - - '@dmsnell/diff-match-patch@1.1.0': {} - - '@emnapi/core@1.7.0': - dependencies: - '@emnapi/wasi-threads': 1.1.0 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.7.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.1.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.27.2': - optional: true - - '@esbuild/android-arm64@0.27.2': - optional: true - - '@esbuild/android-arm@0.27.2': - optional: true - - '@esbuild/android-x64@0.27.2': - optional: true - - '@esbuild/darwin-arm64@0.27.2': - optional: true - - '@esbuild/darwin-x64@0.27.2': - optional: true - - '@esbuild/freebsd-arm64@0.27.2': - optional: true - - '@esbuild/freebsd-x64@0.27.2': - optional: true - - '@esbuild/linux-arm64@0.27.2': - optional: true - - '@esbuild/linux-arm@0.27.2': - optional: true - - '@esbuild/linux-ia32@0.27.2': - optional: true - - '@esbuild/linux-loong64@0.27.2': - optional: true - - '@esbuild/linux-mips64el@0.27.2': - optional: true - - '@esbuild/linux-ppc64@0.27.2': - optional: true - - '@esbuild/linux-riscv64@0.27.2': - optional: true - - '@esbuild/linux-s390x@0.27.2': - optional: true - - '@esbuild/linux-x64@0.27.2': - optional: true - - '@esbuild/netbsd-arm64@0.27.2': - optional: true - - '@esbuild/netbsd-x64@0.27.2': - optional: true - - '@esbuild/openbsd-arm64@0.27.2': - optional: true - - '@esbuild/openbsd-x64@0.27.2': - optional: true - - '@esbuild/openharmony-arm64@0.27.2': - optional: true - - '@esbuild/sunos-x64@0.27.2': - optional: true - - '@esbuild/win32-arm64@0.27.2': - optional: true - - '@esbuild/win32-ia32@0.27.2': - optional: true - - '@esbuild/win32-x64@0.27.2': - optional: true - - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2)': - dependencies: - eslint: 9.39.2 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.21.1': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@10.2.2) - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 - - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.1': - dependencies: - ajv: 6.12.6 - debug: 4.4.3(supports-color@10.2.2) - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.2': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@inquirer/ansi@1.0.2': {} - - '@inquirer/confirm@5.1.21(@types/node@24.10.1)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.1) - '@inquirer/type': 3.0.10(@types/node@24.10.1) - optionalDependencies: - '@types/node': 24.10.1 - - '@inquirer/core@10.3.2(@types/node@24.10.1)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.1) - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 24.10.1 - - '@inquirer/figures@1.0.15': {} - - '@inquirer/type@3.0.10(@types/node@24.10.1)': - optionalDependencies: - '@types/node': 24.10.1 - - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@lit-labs/ssr-dom-shim@1.5.1': {} - - '@lit/context@1.1.6': - dependencies: - '@lit/reactive-element': 2.1.2 - - '@lit/react@1.0.8(@types/react@19.2.7)': - dependencies: - '@types/react': 19.2.7 - - '@lit/reactive-element@2.1.2': - dependencies: - '@lit-labs/ssr-dom-shim': 1.5.1 - - '@microsoft/api-extractor-model@7.32.0(@types/node@24.10.1)': - dependencies: - '@microsoft/tsdoc': 0.16.0 - '@microsoft/tsdoc-config': 0.18.0 - '@rushstack/node-core-library': 5.18.0(@types/node@24.10.1) - transitivePeerDependencies: - - '@types/node' - - '@microsoft/api-extractor@7.55.0(@types/node@24.10.1)': - dependencies: - '@microsoft/api-extractor-model': 7.32.0(@types/node@24.10.1) - '@microsoft/tsdoc': 0.16.0 - '@microsoft/tsdoc-config': 0.18.0 - '@rushstack/node-core-library': 5.18.0(@types/node@24.10.1) - '@rushstack/rig-package': 0.6.0 - '@rushstack/terminal': 0.19.3(@types/node@24.10.1) - '@rushstack/ts-command-line': 5.1.3(@types/node@24.10.1) - diff: 8.0.2 - lodash: 4.17.21 - minimatch: 10.0.3 - resolve: 1.22.11 - semver: 7.5.4 - source-map: 0.6.1 - typescript: 5.8.2 - transitivePeerDependencies: - - '@types/node' - - '@microsoft/tsdoc-config@0.18.0': - dependencies: - '@microsoft/tsdoc': 0.16.0 - ajv: 8.12.0 - jju: 1.4.0 - resolve: 1.22.11 - - '@microsoft/tsdoc@0.16.0': {} - - '@msw/playwright@0.4.5(msw@2.12.8(@types/node@24.10.1)(typescript@5.9.3))': - dependencies: - '@mswjs/interceptors': 0.40.0 - msw: 2.12.8(@types/node@24.10.1)(typescript@5.9.3) - outvariant: 1.4.3 - - '@mswjs/interceptors@0.40.0': - dependencies: - '@open-draft/deferred-promise': 2.2.0 - '@open-draft/logger': 0.3.0 - '@open-draft/until': 2.1.0 - is-node-process: 1.2.0 - outvariant: 1.4.3 - strict-event-emitter: 0.5.1 - - '@mswjs/interceptors@0.41.0': - dependencies: - '@open-draft/deferred-promise': 2.2.0 - '@open-draft/logger': 0.3.0 - '@open-draft/until': 2.1.0 - is-node-process: 1.2.0 - outvariant: 1.4.3 - strict-event-emitter: 0.5.1 - - '@napi-rs/wasm-runtime@0.2.12': - dependencies: - '@emnapi/core': 1.7.0 - '@emnapi/runtime': 1.7.0 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@open-draft/deferred-promise@2.2.0': {} - - '@open-draft/logger@0.3.0': - dependencies: - is-node-process: 1.2.0 - outvariant: 1.4.3 - - '@open-draft/until@2.1.0': {} - - '@parcel/watcher-android-arm64@2.5.1': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.1': - optional: true - - '@parcel/watcher-darwin-x64@2.5.1': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.1': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.1': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.1': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.1': - optional: true - - '@parcel/watcher-win32-arm64@2.5.1': - optional: true - - '@parcel/watcher-win32-ia32@2.5.1': - optional: true - - '@parcel/watcher-win32-x64@2.5.1': - optional: true - - '@parcel/watcher@2.5.1': - dependencies: - detect-libc: 1.0.3 - is-glob: 4.0.3 - micromatch: 4.0.8 - node-addon-api: 7.1.1 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.1 - '@parcel/watcher-darwin-arm64': 2.5.1 - '@parcel/watcher-darwin-x64': 2.5.1 - '@parcel/watcher-freebsd-x64': 2.5.1 - '@parcel/watcher-linux-arm-glibc': 2.5.1 - '@parcel/watcher-linux-arm-musl': 2.5.1 - '@parcel/watcher-linux-arm64-glibc': 2.5.1 - '@parcel/watcher-linux-arm64-musl': 2.5.1 - '@parcel/watcher-linux-x64-glibc': 2.5.1 - '@parcel/watcher-linux-x64-musl': 2.5.1 - '@parcel/watcher-win32-arm64': 2.5.1 - '@parcel/watcher-win32-ia32': 2.5.1 - '@parcel/watcher-win32-x64': 2.5.1 - optional: true - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@playwright/test@1.58.1': - dependencies: - playwright: 1.58.1 - - '@popperjs/core@2.11.8': {} - - '@redocly/ajv@8.17.1': - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - '@redocly/config@0.22.2': {} - - '@redocly/openapi-core@1.34.6(supports-color@10.2.2)': - dependencies: - '@redocly/ajv': 8.17.1 - '@redocly/config': 0.22.2 - colorette: 1.4.0 - https-proxy-agent: 7.0.6(supports-color@10.2.2) - js-levenshtein: 1.1.6 - js-yaml: 4.1.0 - minimatch: 5.1.6 - pluralize: 8.0.0 - yaml-ast-parser: 0.0.43 - transitivePeerDependencies: - - supports-color - - '@rolldown/pluginutils@1.0.0-rc.3': {} - - '@rollup/pluginutils@5.3.0(rollup@4.52.5)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.52.5 - - '@rollup/rollup-android-arm-eabi@4.52.5': - optional: true - - '@rollup/rollup-android-arm64@4.52.5': - optional: true - - '@rollup/rollup-darwin-arm64@4.52.5': - optional: true - - '@rollup/rollup-darwin-x64@4.52.5': - optional: true - - '@rollup/rollup-freebsd-arm64@4.52.5': - optional: true - - '@rollup/rollup-freebsd-x64@4.52.5': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.52.5': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.52.5': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.52.5': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.52.5': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.52.5': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.52.5': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.52.5': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.52.5': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.52.5': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.52.5': - optional: true - - '@rollup/rollup-linux-x64-musl@4.52.5': - optional: true - - '@rollup/rollup-openharmony-arm64@4.52.5': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.52.5': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.52.5': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.52.5': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.52.5': - optional: true - - '@rtsao/scc@1.1.0': {} - - '@rushstack/node-core-library@5.18.0(@types/node@24.10.1)': - dependencies: - ajv: 8.13.0 - ajv-draft-04: 1.0.0(ajv@8.13.0) - ajv-formats: 3.0.1(ajv@8.13.0) - fs-extra: 11.3.2 - import-lazy: 4.0.0 - jju: 1.4.0 - resolve: 1.22.11 - semver: 7.5.4 - optionalDependencies: - '@types/node': 24.10.1 - - '@rushstack/problem-matcher@0.1.1(@types/node@24.10.1)': - optionalDependencies: - '@types/node': 24.10.1 - - '@rushstack/rig-package@0.6.0': - dependencies: - resolve: 1.22.11 - strip-json-comments: 3.1.1 - - '@rushstack/terminal@0.19.3(@types/node@24.10.1)': - dependencies: - '@rushstack/node-core-library': 5.18.0(@types/node@24.10.1) - '@rushstack/problem-matcher': 0.1.1(@types/node@24.10.1) - supports-color: 8.1.1 - optionalDependencies: - '@types/node': 24.10.1 - - '@rushstack/ts-command-line@5.1.3(@types/node@24.10.1)': - dependencies: - '@rushstack/terminal': 0.19.3(@types/node@24.10.1) - '@types/argparse': 1.0.38 - argparse: 1.0.10 - string-argv: 0.3.2 - transitivePeerDependencies: - - '@types/node' - - '@se-oss/deasync-darwin-arm64@1.0.1': - optional: true - - '@se-oss/deasync-darwin-x64@1.0.1': - optional: true - - '@se-oss/deasync-linux-arm64-gnu@1.0.1': - optional: true - - '@se-oss/deasync-linux-arm64-musl@1.0.1': - optional: true - - '@se-oss/deasync-linux-x64-gnu@1.0.1': - optional: true - - '@se-oss/deasync-linux-x64-musl@1.0.1': - optional: true - - '@se-oss/deasync-win32-arm64-msvc@1.0.1': - optional: true - - '@se-oss/deasync-win32-x64-msvc@1.0.1': - optional: true - - '@se-oss/deasync@1.0.1': - dependencies: - type-fest: 4.41.0 - optionalDependencies: - '@se-oss/deasync-darwin-arm64': 1.0.1 - '@se-oss/deasync-darwin-x64': 1.0.1 - '@se-oss/deasync-linux-arm64-gnu': 1.0.1 - '@se-oss/deasync-linux-arm64-musl': 1.0.1 - '@se-oss/deasync-linux-x64-gnu': 1.0.1 - '@se-oss/deasync-linux-x64-musl': 1.0.1 - '@se-oss/deasync-win32-arm64-msvc': 1.0.1 - '@se-oss/deasync-win32-x64-msvc': 1.0.1 - - '@solid-primitives/refs@1.1.2(solid-js@1.9.10)': - dependencies: - '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) - solid-js: 1.9.10 - - '@solid-primitives/transition-group@1.1.2(solid-js@1.9.10)': - dependencies: - solid-js: 1.9.10 - - '@solid-primitives/utils@6.3.2(solid-js@1.9.10)': - dependencies: - solid-js: 1.9.10 - - '@standard-schema/spec@1.1.0': {} - - '@swc/helpers@0.5.17': - dependencies: - tslib: 2.8.1 - - '@tanstack/query-core@5.90.12': {} - - '@tanstack/query-devtools@5.91.1': {} - - '@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3)': - dependencies: - '@tanstack/query-devtools': 5.91.1 - '@tanstack/react-query': 5.90.12(react@19.2.3) - react: 19.2.3 - - '@tanstack/react-query@5.90.12(react@19.2.3)': - dependencies: - '@tanstack/query-core': 5.90.12 - react: 19.2.3 - - '@tanstack/react-table@8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@tanstack/table-core': 8.21.3 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - - '@tanstack/react-virtual@3.13.12(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@tanstack/virtual-core': 3.13.12 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - - '@tanstack/table-core@8.21.3': {} - - '@tanstack/virtual-core@3.13.12': {} - - '@testing-library/dom@10.4.1': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.28.6 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - picocolors: 1.1.1 - pretty-format: 27.5.1 - - '@testing-library/react@16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@babel/runtime': 7.28.4 - '@testing-library/dom': 10.4.1 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) - - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/argparse@1.0.38': {} - - '@types/aria-query@5.0.4': {} - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.28.5 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.28.5 - - '@types/bootstrap@5.2.10': - dependencies: - '@popperjs/core': 2.11.8 - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/command-line-args@5.2.3': {} - - '@types/command-line-usage@5.0.4': {} - - '@types/css-modules@1.0.5': {} - - '@types/deep-eql@4.0.2': {} - - '@types/eslint__js@9.14.0': - dependencies: - '@eslint/js': 9.39.2 - - '@types/estree@1.0.8': {} - - '@types/json-schema@7.0.15': {} - - '@types/json5@0.0.29': {} - - '@types/json5@2.2.0': - dependencies: - json5: 2.2.3 - - '@types/linkify-it@5.0.0': {} - - '@types/lodash-es@4.17.12': - dependencies: - '@types/lodash': 4.17.23 - - '@types/lodash@4.17.23': {} - - '@types/lz4js@0.2.2': {} - - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - - '@types/mdurl@2.0.0': {} - - '@types/node@24.10.1': - dependencies: - undici-types: 7.16.0 - - '@types/postcss-modules-local-by-default@4.0.2': - dependencies: - postcss: 8.5.6 - - '@types/postcss-modules-scope@3.0.4': - dependencies: - postcss: 8.5.6 - - '@types/prismjs@1.26.5': {} - - '@types/react-dom@19.2.3(@types/react@19.2.7)': - dependencies: - '@types/react': 19.2.7 - - '@types/react@19.2.7': - dependencies: - csstype: 3.2.3 - - '@types/statuses@2.0.6': {} - - '@types/trusted-types@2.0.7': {} - - '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.50.0 - eslint: 9.39.2 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.50.0(eslint@9.39.2)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.50.0 - debug: 4.4.3(supports-color@10.2.2) - eslint: 9.39.2 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) - '@typescript-eslint/types': 8.50.0 - debug: 4.4.3(supports-color@10.2.2) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.50.0': - dependencies: - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/visitor-keys': 8.50.0 - - '@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/type-utils@8.50.0(eslint@9.39.2)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - debug: 4.4.3(supports-color@10.2.2) - eslint: 9.39.2 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.50.0': {} - - '@typescript-eslint/typescript-estree@8.50.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.50.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/visitor-keys': 8.50.0 - debug: 4.4.3(supports-color@10.2.2) - minimatch: 9.0.5 - semver: 7.7.3 - tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.50.0(eslint@9.39.2)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - eslint: 9.39.2 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.50.0': - dependencies: - '@typescript-eslint/types': 8.50.0 - eslint-visitor-keys: 4.2.1 - - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - optional: true - - '@unrs/resolver-binding-android-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - dependencies: - '@napi-rs/wasm-runtime': 0.2.12 - optional: true - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - optional: true - - '@uwdata/flechette@2.2.6': {} - - '@vitejs/plugin-react@5.1.4(vite@7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1))': - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) - '@rolldown/pluginutils': 1.0.0-rc.3 - '@types/babel__core': 7.20.5 - react-refresh: 0.18.0 - vite: 7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1) - transitivePeerDependencies: - - supports-color - - '@vitest/expect@4.0.16': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.0.16 - '@vitest/utils': 4.0.16 - chai: 6.2.1 - tinyrainbow: 3.0.3 - - '@vitest/mocker@4.0.16(msw@2.12.8(@types/node@24.10.1)(typescript@5.9.3))(vite@7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 4.0.16 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - msw: 2.12.8(@types/node@24.10.1)(typescript@5.9.3) - vite: 7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1) - - '@vitest/pretty-format@4.0.16': - dependencies: - tinyrainbow: 3.0.3 - - '@vitest/runner@4.0.16': - dependencies: - '@vitest/utils': 4.0.16 - pathe: 2.0.3 - - '@vitest/snapshot@4.0.16': - dependencies: - '@vitest/pretty-format': 4.0.16 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.0.16': {} - - '@vitest/utils@4.0.16': - dependencies: - '@vitest/pretty-format': 4.0.16 - tinyrainbow: 3.0.3 - - '@volar/language-core@2.4.23': - dependencies: - '@volar/source-map': 2.4.23 - - '@volar/source-map@2.4.23': {} - - '@volar/typescript@2.4.23': - dependencies: - '@volar/language-core': 2.4.23 - path-browserify: 1.0.1 - vscode-uri: 3.1.0 - - '@vscode-elements/elements@2.4.0(@vscode/codicons@0.0.44)': - dependencies: - '@lit/context': 1.1.6 - '@vscode/codicons': 0.0.44 - lit: 3.3.2 - - '@vscode-elements/react-elements@2.4.0(@types/react@19.2.7)(@vscode/codicons@0.0.44)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@lit/react': 1.0.8(@types/react@19.2.7) - '@vscode-elements/elements': 2.4.0(@vscode/codicons@0.0.44) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - transitivePeerDependencies: - - '@types/react' - - '@vscode/codicons' - - '@vscode/codicons@0.0.44': {} - - '@vue/compiler-core@3.5.24': - dependencies: - '@babel/parser': 7.28.5 - '@vue/shared': 3.5.24 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - - '@vue/compiler-dom@3.5.24': - dependencies: - '@vue/compiler-core': 3.5.24 - '@vue/shared': 3.5.24 - - '@vue/compiler-vue2@2.7.16': - dependencies: - de-indent: 1.0.2 - he: 1.2.0 - - '@vue/language-core@2.2.0(typescript@5.9.3)': - dependencies: - '@volar/language-core': 2.4.23 - '@vue/compiler-dom': 3.5.24 - '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.24 - alien-signals: 0.4.14 - minimatch: 9.0.5 - muggle-string: 0.4.1 - path-browserify: 1.0.1 - optionalDependencies: - typescript: 5.9.3 - - '@vue/shared@3.5.24': {} - - '@xmldom/xmldom@0.9.8': {} - - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - - ag-charts-types@12.3.1: {} - - ag-grid-community@34.3.1: - dependencies: - ag-charts-types: 12.3.1 - - ag-grid-react@34.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): - dependencies: - ag-grid-community: 34.3.1 - prop-types: 15.8.1 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - - agent-base@7.1.4: {} - - ajv-draft-04@1.0.0(ajv@8.13.0): - optionalDependencies: - ajv: 8.13.0 - - ajv-formats@3.0.1(ajv@8.13.0): - optionalDependencies: - ajv: 8.13.0 - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ajv@8.12.0: - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - - ajv@8.13.0: - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - - alien-signals@0.4.14: {} - - ansi-colors@4.1.3: {} - - ansi-output@0.0.9: {} - - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - ansi-styles@6.2.3: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - apache-arrow@21.1.0: - dependencies: - '@swc/helpers': 0.5.17 - '@types/command-line-args': 5.2.3 - '@types/command-line-usage': 5.0.4 - '@types/node': 24.10.1 - command-line-args: 6.0.1 - command-line-usage: 7.0.3 - flatbuffers: 25.9.23 - json-bignum: 0.0.3 - tslib: 2.8.1 - transitivePeerDependencies: - - '@75lb/nature' - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - argparse@2.0.1: {} - - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - - arquero@8.0.3: - dependencies: - '@uwdata/flechette': 2.2.6 - acorn: 8.15.0 - - array-back@6.2.2: {} - - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-includes@3.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - - array.prototype.findlast@1.2.5: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.findlastindex@1.2.6: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - - array.prototype.tosorted@1.1.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - asciinema-player@3.13.5: - dependencies: - '@babel/runtime': 7.28.4 - solid-js: 1.9.10 - solid-transition-group: 0.2.3(solid-js@1.9.10) - - assertion-error@2.0.1: {} - - async-function@1.0.0: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - balanced-match@1.0.2: {} - - baseline-browser-mapping@2.9.19: {} - - bidi-js@1.0.3: - dependencies: - require-from-string: 2.0.2 - - binary-extensions@2.3.0: {} - - bootstrap-icons@1.13.1: {} - - bootstrap@5.3.8(@popperjs/core@2.11.8): - dependencies: - '@popperjs/core': 2.11.8 - - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.28.1: - dependencies: - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001769 - electron-to-chromium: 1.5.286 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.8: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - callsites@3.1.0: {} - - camelcase@6.3.0: {} - - caniuse-lite@1.0.30001769: {} - - chai@6.2.1: {} - - chalk-template@0.4.0: - dependencies: - chalk: 4.1.2 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - change-case@5.4.4: {} - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - cli-width@4.1.0: {} - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - clsx@2.1.1: {} - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - colorette@1.4.0: {} - - command-line-args@6.0.1: - dependencies: - array-back: 6.2.2 - find-replace: 5.0.2 - lodash.camelcase: 4.3.0 - typical: 7.3.0 - - command-line-usage@7.0.3: - dependencies: - array-back: 6.2.2 - chalk-template: 0.4.0 - table-layout: 4.1.1 - typical: 7.3.0 - - commander@13.1.0: {} - - compare-versions@6.1.1: {} - - concat-map@0.0.1: {} - - confbox@0.1.8: {} - - confbox@0.2.2: {} - - convert-source-map@2.0.0: {} - - cookie@1.0.2: {} - - copy-anything@2.0.6: - dependencies: - is-what: 3.14.1 - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - css-tree@3.1.0: - dependencies: - mdn-data: 2.12.2 - source-map-js: 1.2.1 - - cssesc@3.0.0: {} - - cssstyle@5.3.5: - dependencies: - '@asamuzakjp/css-color': 4.1.1 - '@csstools/css-syntax-patches-for-csstree': 1.0.21 - css-tree: 3.1.0 - - csstype@3.1.3: {} - - csstype@3.2.3: {} - - cuint@0.2.2: {} - - data-urls@6.0.0: - dependencies: - whatwg-mimetype: 4.0.0 - whatwg-url: 15.1.0 - - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - de-indent@1.0.2: {} - - debug@3.2.7: - dependencies: - ms: 2.1.3 - - debug@4.4.3(supports-color@10.2.2): - dependencies: - ms: 2.1.3 - optionalDependencies: - supports-color: 10.2.2 - - decimal.js@10.6.0: {} - - deep-is@0.1.4: {} - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - dequal@2.0.3: {} - - detect-libc@1.0.3: - optional: true - - diff@8.0.2: {} - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - dom-accessibility-api@0.5.16: {} - - dotenv@16.6.1: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - eastasianwidth@0.2.0: {} - - electron-to-chromium@1.5.286: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - entities@4.5.0: {} - - entities@6.0.1: {} - - errno@0.1.8: - dependencies: - prr: 1.0.1 - optional: true - - es-abstract@1.24.0: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-iterator-helpers@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-set-tostringtag: 2.1.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - iterator.prototype: 1.1.5 - safe-array-concat: 1.1.3 - - es-module-lexer@1.7.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.2 - - es-to-primitive@1.3.0: - dependencies: - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - - esbuild@0.27.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.2 - '@esbuild/android-arm': 0.27.2 - '@esbuild/android-arm64': 0.27.2 - '@esbuild/android-x64': 0.27.2 - '@esbuild/darwin-arm64': 0.27.2 - '@esbuild/darwin-x64': 0.27.2 - '@esbuild/freebsd-arm64': 0.27.2 - '@esbuild/freebsd-x64': 0.27.2 - '@esbuild/linux-arm': 0.27.2 - '@esbuild/linux-arm64': 0.27.2 - '@esbuild/linux-ia32': 0.27.2 - '@esbuild/linux-loong64': 0.27.2 - '@esbuild/linux-mips64el': 0.27.2 - '@esbuild/linux-ppc64': 0.27.2 - '@esbuild/linux-riscv64': 0.27.2 - '@esbuild/linux-s390x': 0.27.2 - '@esbuild/linux-x64': 0.27.2 - '@esbuild/netbsd-arm64': 0.27.2 - '@esbuild/netbsd-x64': 0.27.2 - '@esbuild/openbsd-arm64': 0.27.2 - '@esbuild/openbsd-x64': 0.27.2 - '@esbuild/openharmony-arm64': 0.27.2 - '@esbuild/sunos-x64': 0.27.2 - '@esbuild/win32-arm64': 0.27.2 - '@esbuild/win32-ia32': 0.27.2 - '@esbuild/win32-x64': 0.27.2 - - escalade@3.2.0: {} - - escape-string-regexp@4.0.0: {} - - eslint-config-prettier@10.1.8(eslint@9.39.2): - dependencies: - eslint: 9.39.2 - - eslint-import-context@0.1.9(unrs-resolver@1.11.1): - dependencies: - get-tsconfig: 4.13.0 - stable-hash-x: 0.2.0 - optionalDependencies: - unrs-resolver: 1.11.1 - - eslint-import-resolver-node@0.3.9: - dependencies: - debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 1.22.11 - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2): - dependencies: - debug: 4.4.3(supports-color@10.2.2) - eslint: 9.39.2 - eslint-import-context: 0.1.9(unrs-resolver@1.11.1) - get-tsconfig: 4.13.0 - is-bun-module: 2.0.0 - stable-hash-x: 0.2.0 - tinyglobby: 0.2.15 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2) - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 9.39.2 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-react-hooks@7.0.1(eslint@9.39.2): - dependencies: - '@babel/core': 7.28.5 - '@babel/parser': 7.28.5 - eslint: 9.39.2 - hermes-parser: 0.25.1 - zod: 4.1.12 - zod-validation-error: 4.0.2(zod@4.1.12) - transitivePeerDependencies: - - supports-color - - eslint-plugin-react-refresh@0.4.26(eslint@9.39.2): - dependencies: - eslint: 9.39.2 - - eslint-plugin-react@7.37.5(eslint@9.39.2): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.1 - eslint: 9.39.2 - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint@9.39.2: - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.39.2 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@10.2.2) - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - esm@3.2.25: {} - - espree@10.4.0: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 4.2.1 - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@2.0.2: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - esutils@2.0.3: {} - - eventsource-parser@3.0.6: {} - - eventsource@4.1.0: - dependencies: - eventsource-parser: 3.0.6 - - expect-type@1.3.0: {} - - exsolve@1.0.8: {} - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fast-uri@3.1.0: {} - - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-replace@5.0.2: {} - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - - flatbuffers@25.9.23: {} - - flatted@3.3.3: {} - - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - fs-extra@11.3.2: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - - fs.realpath@1.0.0: - optional: true - - fsevents@2.3.2: - optional: true - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - function.prototype.name@1.1.8: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - functions-have-names: 1.2.3 - hasown: 2.0.2 - is-callable: 1.2.7 - - functions-have-names@1.2.3: {} - - fzstd@0.1.1: {} - - generator-function@2.0.1: {} - - gensync@1.0.0-beta.2: {} - - get-caller-file@2.0.5: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - - get-tsconfig@4.13.0: - dependencies: - resolve-pkg-maps: 1.0.0 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@10.4.5: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - optional: true - - globals@14.0.0: {} - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - - gopd@1.2.0: {} - - graceful-fs@4.2.11: {} - - graphql@16.12.0: {} - - has-bigints@1.1.0: {} - - has-flag@4.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - he@1.2.0: {} - - headers-polyfill@4.0.3: {} - - hermes-estree@0.25.1: {} - - hermes-parser@0.25.1: - dependencies: - hermes-estree: 0.25.1 - - html-encoding-sniffer@4.0.0: - dependencies: - whatwg-encoding: 3.1.1 - - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - - https-proxy-agent@7.0.6(supports-color@10.2.2): - dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - icss-replace-symbols@1.1.0: {} - - icss-utils@5.1.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - - ignore@5.3.2: {} - - ignore@7.0.5: {} - - image-size@0.5.5: - optional: true - - immer@10.2.0: {} - - immutable@5.1.4: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - import-lazy@4.0.0: {} - - imurmurhash@0.1.4: {} - - index-to-position@1.2.0: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - optional: true - - inherits@2.0.4: - optional: true - - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-bun-module@2.0.0: - dependencies: - semver: 7.7.3 - - is-callable@1.2.7: {} - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-extglob@2.1.1: {} - - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-fullwidth-code-point@3.0.0: {} - - is-generator-function@1.1.2: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.1 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-map@2.0.3: {} - - is-negative-zero@2.0.3: {} - - is-node-process@1.2.0: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-number@7.0.0: {} - - is-potential-custom-element-name@1.0.1: {} - - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-there@4.5.2: {} - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.19 - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-what@3.14.1: {} - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - iterator.prototype@1.1.5: - dependencies: - define-data-property: 1.1.4 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - has-symbols: 1.1.0 - set-function-name: 2.0.2 - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jju@1.4.0: {} - - js-levenshtein@1.1.6: {} - - js-tokens@4.0.0: {} - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - jsdom@27.3.0: - dependencies: - '@acemir/cssom': 0.9.29 - '@asamuzakjp/dom-selector': 6.7.6 - cssstyle: 5.3.5 - data-urls: 6.0.0 - decimal.js: 10.6.0 - html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6(supports-color@10.2.2) - is-potential-custom-element-name: 1.0.1 - parse5: 8.0.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 6.0.0 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 8.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 15.1.0 - ws: 8.18.3 - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - jsesc@3.1.0: {} - - json-bignum@0.0.3: {} - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-schema-traverse@1.0.0: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@1.0.2: - dependencies: - minimist: 1.2.8 - - json5@2.2.3: {} - - jsondiffpatch@0.7.3: - dependencies: - '@dmsnell/diff-match-patch': 1.1.0 - - jsonfile@6.2.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - jsx-ast-utils@3.3.5: - dependencies: - array-includes: 3.1.9 - array.prototype.flat: 1.3.3 - object.assign: 4.1.7 - object.values: 1.2.1 - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - kolorist@1.8.0: {} - - less@4.4.2: - dependencies: - copy-anything: 2.0.6 - parse-node-version: 1.0.1 - tslib: 2.8.1 - optionalDependencies: - errno: 0.1.8 - graceful-fs: 4.2.11 - image-size: 0.5.5 - make-dir: 2.1.0 - mime: 1.6.0 - needle: 3.3.1 - source-map: 0.6.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lilconfig@2.1.0: {} - - linkify-it@5.0.0: - dependencies: - uc.micro: 2.1.0 - - lit-element@4.2.2: - dependencies: - '@lit-labs/ssr-dom-shim': 1.5.1 - '@lit/reactive-element': 2.1.2 - lit-html: 3.3.2 - - lit-html@3.3.2: - dependencies: - '@types/trusted-types': 2.0.7 - - lit@3.3.2: - dependencies: - '@lit/reactive-element': 2.1.2 - lit-element: 4.2.2 - lit-html: 3.3.2 - - local-pkg@1.1.2: - dependencies: - mlly: 1.8.0 - pkg-types: 2.3.0 - quansync: 0.2.11 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash-es@4.17.23: {} - - lodash.camelcase@4.3.0: {} - - lodash.merge@4.6.2: {} - - lodash@4.17.21: {} - - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - - lru-cache@10.4.3: {} - - lru-cache@11.2.4: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - - lz-string@1.5.0: {} - - lz4js@0.2.0: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - make-dir@2.1.0: - dependencies: - pify: 4.0.1 - semver: 5.7.2 - optional: true - - make-dir@3.1.0: - dependencies: - semver: 6.3.1 - - markdown-it-mathjax3@5.2.0: - dependencies: - '@se-oss/deasync': 1.0.1 - mathxyjax3: 0.8.3 - - markdown-it@14.1.0: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - - math-intrinsics@1.1.0: {} - - mathjax-full@3.2.2: - dependencies: - esm: 3.2.25 - mhchemparser: 4.2.1 - mj-context-menu: 0.6.1 - speech-rule-engine: 4.1.2 - - mathxyjax3@0.8.3: {} - - mdn-data@2.12.2: {} - - mdurl@2.0.0: {} - - mhchemparser@4.2.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - optional: true - - mime@1.6.0: - optional: true - - mime@2.5.2: {} - - minimatch@10.0.3: - dependencies: - '@isaacs/brace-expansion': 5.0.0 - - minimatch@3.0.8: - dependencies: - brace-expansion: 1.1.12 - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.2 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - - minimist@1.2.8: {} - - minipass@7.1.2: {} - - mj-context-menu@0.6.1: {} - - mkdirp@3.0.1: {} - - mlly@1.8.0: - dependencies: - acorn: 8.15.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.1 - - ms@2.1.3: {} - - msw@2.12.8(@types/node@24.10.1)(typescript@5.9.3): - dependencies: - '@inquirer/confirm': 5.1.21(@types/node@24.10.1) - '@mswjs/interceptors': 0.41.0 - '@open-draft/deferred-promise': 2.2.0 - '@types/statuses': 2.0.6 - cookie: 1.0.2 - graphql: 16.12.0 - headers-polyfill: 4.0.3 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - rettime: 0.10.1 - statuses: 2.0.2 - strict-event-emitter: 0.5.1 - tough-cookie: 6.0.0 - type-fest: 5.4.3 - until-async: 3.0.2 - yargs: 17.7.2 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - '@types/node' - - muggle-string@0.4.1: {} - - mute-stream@2.0.0: {} - - nanoid@3.3.11: {} - - napi-postinstall@0.3.4: {} - - natural-compare@1.4.0: {} - - needle@3.3.1: - dependencies: - iconv-lite: 0.6.3 - sax: 1.4.1 - optional: true - - node-addon-api@7.1.1: - optional: true - - node-releases@2.0.27: {} - - normalize-path@3.0.0: {} - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.entries@1.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - obug@2.1.1: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - optional: true - - openapi-typescript@7.10.1(typescript@5.9.3): - dependencies: - '@redocly/openapi-core': 1.34.6(supports-color@10.2.2) - ansi-colors: 4.1.3 - change-case: 5.4.4 - parse-json: 8.3.0 - supports-color: 10.2.2 - typescript: 5.9.3 - yargs-parser: 21.1.1 - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - outvariant@1.4.3: {} - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - package-json-from-dist@1.0.1: {} - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-json@8.3.0: - dependencies: - '@babel/code-frame': 7.27.1 - index-to-position: 1.2.0 - type-fest: 4.41.0 - - parse-node-version@1.0.1: {} - - parse5@8.0.0: - dependencies: - entities: 6.0.1 - - path-browserify@1.0.1: {} - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: - optional: true - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - - path-to-regexp@6.3.0: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - picomatch@4.0.3: {} - - pify@4.0.1: - optional: true - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.0 - pathe: 2.0.3 - - pkg-types@2.3.0: - dependencies: - confbox: 0.2.2 - exsolve: 1.0.8 - pathe: 2.0.3 - - playwright-core@1.58.1: {} - - playwright@1.58.1: - dependencies: - playwright-core: 1.58.1 - optionalDependencies: - fsevents: 2.3.2 - - pluralize@8.0.0: {} - - possible-typed-array-names@1.1.0: {} - - postcss-load-config@3.1.4(postcss@8.5.6): - dependencies: - lilconfig: 2.1.0 - yaml: 2.8.1 - optionalDependencies: - postcss: 8.5.6 - - postcss-modules-extract-imports@3.1.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - - postcss-modules-local-by-default@4.2.0(postcss@8.5.6): - dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-selector-parser: 7.1.0 - postcss-value-parser: 4.2.0 - - postcss-modules-scope@3.2.1(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - postcss-selector-parser: 7.1.0 - - postcss-modules-values@4.0.0(postcss@8.5.6): - dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 - - postcss-selector-parser@7.1.0: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-url@10.1.3(postcss@8.5.6): - dependencies: - make-dir: 3.1.0 - mime: 2.5.2 - minimatch: 3.0.8 - postcss: 8.5.6 - xxhashjs: 0.2.2 - - postcss-value-parser@4.2.0: {} - - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - prettier@3.7.4: {} - - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - - prismjs@1.30.0: {} - - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - - prr@1.0.1: - optional: true - - punycode.js@2.3.1: {} - - punycode@2.3.1: {} - - quansync@0.2.11: {} - - react-dom@19.2.3(react@19.2.3): - dependencies: - react: 19.2.3 - scheduler: 0.27.0 - - react-fast-compare@3.2.2: {} - - react-is@16.13.1: {} - - react-is@17.0.2: {} - - react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): - dependencies: - '@popperjs/core': 2.11.8 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-fast-compare: 3.2.2 - warning: 4.0.3 - - react-refresh@0.18.0: {} - - react-router-dom@7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): - dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-router: 7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - - react-router@7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): - dependencies: - cookie: 1.0.2 - react: 19.2.3 - set-cookie-parser: 2.7.2 - optionalDependencies: - react-dom: 19.2.3(react@19.2.3) - - react-virtuoso@4.17.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): - dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - - react@19.2.3: {} - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - readdirp@4.1.2: {} - - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - - require-directory@2.1.1: {} - - require-from-string@2.0.2: {} - - reserved-words@0.1.2: {} - - resolve-from@4.0.0: {} - - resolve-pkg-maps@1.0.0: {} - - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - resolve@2.0.0-next.5: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - rettime@0.10.1: {} - - rollup@4.52.5: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.5 - '@rollup/rollup-android-arm64': 4.52.5 - '@rollup/rollup-darwin-arm64': 4.52.5 - '@rollup/rollup-darwin-x64': 4.52.5 - '@rollup/rollup-freebsd-arm64': 4.52.5 - '@rollup/rollup-freebsd-x64': 4.52.5 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 - '@rollup/rollup-linux-arm-musleabihf': 4.52.5 - '@rollup/rollup-linux-arm64-gnu': 4.52.5 - '@rollup/rollup-linux-arm64-musl': 4.52.5 - '@rollup/rollup-linux-loong64-gnu': 4.52.5 - '@rollup/rollup-linux-ppc64-gnu': 4.52.5 - '@rollup/rollup-linux-riscv64-gnu': 4.52.5 - '@rollup/rollup-linux-riscv64-musl': 4.52.5 - '@rollup/rollup-linux-s390x-gnu': 4.52.5 - '@rollup/rollup-linux-x64-gnu': 4.52.5 - '@rollup/rollup-linux-x64-musl': 4.52.5 - '@rollup/rollup-openharmony-arm64': 4.52.5 - '@rollup/rollup-win32-arm64-msvc': 4.52.5 - '@rollup/rollup-win32-ia32-msvc': 4.52.5 - '@rollup/rollup-win32-x64-gnu': 4.52.5 - '@rollup/rollup-win32-x64-msvc': 4.52.5 - fsevents: 2.3.3 - - safe-array-concat@1.1.3: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - - safer-buffer@2.1.2: {} - - sass@1.93.2: - dependencies: - chokidar: 4.0.3 - immutable: 5.1.4 - source-map-js: 1.2.1 - optionalDependencies: - '@parcel/watcher': 2.5.1 - - sax@1.3.0: - optional: true - - sax@1.4.1: - optional: true - - saxes@6.0.0: - dependencies: - xmlchars: 2.2.0 - - scheduler@0.27.0: {} - - semver@5.7.2: - optional: true - - semver@6.3.1: {} - - semver@7.5.4: - dependencies: - lru-cache: 6.0.0 - - semver@7.7.3: {} - - seroval-plugins@1.3.3(seroval@1.3.2): - dependencies: - seroval: 1.3.2 - - seroval@1.3.2: {} - - set-cookie-parser@2.7.2: {} - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - siginfo@2.0.0: {} - - signal-exit@4.1.0: {} - - solid-js@1.9.10: - dependencies: - csstype: 3.1.3 - seroval: 1.3.2 - seroval-plugins: 1.3.3(seroval@1.3.2) - - solid-transition-group@0.2.3(solid-js@1.9.10): - dependencies: - '@solid-primitives/refs': 1.1.2(solid-js@1.9.10) - '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.10) - solid-js: 1.9.10 - - source-map-js@1.2.1: {} - - source-map@0.6.1: {} - - source-map@0.7.6: - optional: true - - speech-rule-engine@4.1.2: - dependencies: - '@xmldom/xmldom': 0.9.8 - commander: 13.1.0 - wicked-good-xpath: 1.3.0 - - sprintf-js@1.0.3: {} - - stable-hash-x@0.2.0: {} - - stackback@0.0.2: {} - - statuses@2.0.2: {} - - std-env@3.10.0: {} - - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - - strict-event-emitter@0.5.1: {} - - string-argv@0.3.2: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - - string.prototype.matchall@4.0.12: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.4 - set-function-name: 2.0.2 - side-channel: 1.1.0 - - string.prototype.repeat@1.0.0: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.24.0 - - string.prototype.trim@1.2.10: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - has-property-descriptors: 1.0.2 - - string.prototype.trimend@1.0.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.2: - dependencies: - ansi-regex: 6.2.2 - - strip-bom@3.0.0: {} - - strip-json-comments@3.1.1: {} - - stylus@0.62.0: - dependencies: - '@adobe/css-tools': 4.3.3 - debug: 4.4.3(supports-color@10.2.2) - glob: 7.2.3 - sax: 1.3.0 - source-map: 0.7.6 - transitivePeerDependencies: - - supports-color - optional: true - - supports-color@10.2.2: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - symbol-tree@3.2.4: {} - - table-layout@4.1.1: - dependencies: - array-back: 6.2.2 - wordwrapjs: 5.1.1 - - tagged-tag@1.0.0: {} - - tinybench@2.9.0: {} - - tinyexec@1.0.2: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - - tinyrainbow@3.0.3: {} - - tldts-core@7.0.19: {} - - tldts@7.0.19: - dependencies: - tldts-core: 7.0.19 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - tough-cookie@6.0.0: - dependencies: - tldts: 7.0.19 - - tr46@6.0.0: - dependencies: - punycode: 2.3.1 - - ts-api-utils@2.1.0(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - - tsconfig-paths@4.2.0: - dependencies: - json5: 2.2.3 - minimist: 1.2.8 - strip-bom: 3.0.0 - - tslib@2.8.1: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@4.41.0: {} - - type-fest@5.4.3: - dependencies: - tagged-tag: 1.0.0 - - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.7: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - - typed-css-modules@0.9.1: - dependencies: - camelcase: 6.3.0 - chalk: 4.1.2 - chokidar: 3.6.0 - glob: 10.4.5 - icss-replace-symbols: 1.1.0 - is-there: 4.5.2 - mkdirp: 3.0.1 - postcss: 8.5.6 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) - postcss-modules-scope: 3.2.1(postcss@8.5.6) - postcss-modules-values: 4.0.0(postcss@8.5.6) - yargs: 17.7.2 - - typescript-eslint@8.50.0(eslint@9.39.2)(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - typescript-plugin-css-modules@5.2.0(typescript@5.9.3): - dependencies: - '@types/postcss-modules-local-by-default': 4.0.2 - '@types/postcss-modules-scope': 3.0.4 - dotenv: 16.6.1 - icss-utils: 5.1.0(postcss@8.5.6) - less: 4.4.2 - lodash.camelcase: 4.3.0 - postcss: 8.5.6 - postcss-load-config: 3.1.4(postcss@8.5.6) - postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) - postcss-modules-scope: 3.2.1(postcss@8.5.6) - reserved-words: 0.1.2 - sass: 1.93.2 - source-map-js: 1.2.1 - tsconfig-paths: 4.2.0 - typescript: 5.9.3 - optionalDependencies: - stylus: 0.62.0 - transitivePeerDependencies: - - supports-color - - ts-node - - typescript@5.8.2: {} - - typescript@5.9.3: {} - - typical@7.3.0: {} - - uc.micro@2.1.0: {} - - ufo@1.6.1: {} - - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - - undici-types@7.16.0: {} - - universalify@2.0.1: {} - - unrs-resolver@1.11.1: - dependencies: - napi-postinstall: 0.3.4 - optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.1 - '@unrs/resolver-binding-android-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-x64': 1.11.1 - '@unrs/resolver-binding-freebsd-x64': 1.11.1 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-musl': 1.11.1 - '@unrs/resolver-binding-wasm32-wasi': 1.11.1 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - - until-async@3.0.2: {} - - update-browserslist-db@1.2.3(browserslist@4.28.1): - dependencies: - browserslist: 4.28.1 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - util-deprecate@1.0.2: {} - - vite-plugin-dts@4.5.4(@types/node@24.10.1)(rollup@4.52.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1)): - dependencies: - '@microsoft/api-extractor': 7.55.0(@types/node@24.10.1) - '@rollup/pluginutils': 5.3.0(rollup@4.52.5) - '@volar/typescript': 2.4.23 - '@vue/language-core': 2.2.0(typescript@5.9.3) - compare-versions: 6.1.1 - debug: 4.4.3(supports-color@10.2.2) - kolorist: 1.8.0 - local-pkg: 1.1.2 - magic-string: 0.30.21 - typescript: 5.9.3 - optionalDependencies: - vite: 7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1) - transitivePeerDependencies: - - '@types/node' - - rollup - - supports-color - - vite@7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1): - dependencies: - esbuild: 0.27.2 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.52.5 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.10.1 - fsevents: 2.3.3 - less: 4.4.2 - sass: 1.93.2 - stylus: 0.62.0 - yaml: 2.8.1 - - vitest@4.0.16(@types/node@24.10.1)(jsdom@27.3.0)(less@4.4.2)(msw@2.12.8(@types/node@24.10.1)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1): - dependencies: - '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(msw@2.12.8(@types/node@24.10.1)(typescript@5.9.3))(vite@7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1)) - '@vitest/pretty-format': 4.0.16 - '@vitest/runner': 4.0.16 - '@vitest/snapshot': 4.0.16 - '@vitest/spy': 4.0.16 - '@vitest/utils': 4.0.16 - es-module-lexer: 1.7.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.0(@types/node@24.10.1)(less@4.4.2)(sass@1.93.2)(stylus@0.62.0)(yaml@2.8.1) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 24.10.1 - jsdom: 27.3.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - - vscode-uri@3.1.0: {} - - w3c-xmlserializer@5.0.0: - dependencies: - xml-name-validator: 5.0.0 - - warning@4.0.3: - dependencies: - loose-envify: 1.4.0 - - webidl-conversions@8.0.0: {} - - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@4.0.0: {} - - whatwg-url@15.1.0: - dependencies: - tr46: 6.0.0 - webidl-conversions: 8.0.0 - - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.1.8 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.19 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.19: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - wicked-good-xpath@1.3.0: {} - - word-wrap@1.2.5: {} - - wordwrapjs@5.1.1: {} - - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.1.2 - - wrappy@1.0.2: - optional: true - - ws@8.18.3: {} - - xml-name-validator@5.0.0: {} - - xmlchars@2.2.0: {} - - xxhashjs@0.2.2: - dependencies: - cuint: 0.2.2 - - y18n@5.0.8: {} - - yallist@3.1.1: {} - - yallist@4.0.0: {} - - yaml-ast-parser@0.0.43: {} - - yaml@2.8.1: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yocto-queue@0.1.0: {} - - yoctocolors-cjs@2.1.3: {} - - zod-validation-error@4.0.2(zod@4.1.12): - dependencies: - zod: 4.1.12 - - zod@4.1.12: {} - - zstd-codec@0.1.5: {} - - zustand@5.0.9(@types/react@19.2.7)(immer@10.2.0)(react@19.2.3): - optionalDependencies: - '@types/react': 19.2.7 - immer: 10.2.0 - react: 19.2.3 diff --git a/src/inspect_scout/_view/www/postcss.config.cjs b/src/inspect_scout/_view/www/postcss.config.cjs deleted file mode 100644 index 2f2f1e344..000000000 --- a/src/inspect_scout/_view/www/postcss.config.cjs +++ /dev/null @@ -1,11 +0,0 @@ - -// postcss.config.js -module.exports = { - plugins: [ - require("postcss-url")({ - url: "inline", - maxSize: Infinity, - fallback: "copy", - }), - ], -}; diff --git a/src/inspect_scout/_view/www/scripts/generate-types.js b/src/inspect_scout/_view/www/scripts/generate-types.js deleted file mode 100644 index d1eb338fe..000000000 --- a/src/inspect_scout/_view/www/scripts/generate-types.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Generate TypeScript types from OpenAPI schema with custom transforms. - * - * Uses openapi-typescript Node API with postTransform to fix JsonValue type. - * openapi-typescript inlines recursive $refs, causing TS2502 errors. - */ -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import openapiTS, { astToString } from "openapi-typescript"; -import ts from "typescript"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const ROOT = path.resolve(__dirname, ".."); -const SCHEMA_PATH = path.join(ROOT, "openapi.json"); -const OUTPUT_PATH = path.join(ROOT, "src/types/generated.ts"); - -// Create the JsonValue type reference -const jsonValueRef = ts.factory.createTypeReferenceNode("JsonValue"); - -const ast = await openapiTS(new URL(`file://${SCHEMA_PATH}`), { - postTransform(schemaObject, metadata) { - // Replace JsonValue inline definition with reference to our manual type - if (metadata.path?.endsWith("/JsonValue")) { - return jsonValueRef; - } - }, -}); - -// Prepend import for JsonValue -const importDecl = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier("JsonValue"), - ), - ]), - ), - ts.factory.createStringLiteral("./json-value"), -); - -const HEADER = `/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ -`; - -const output = HEADER + astToString([importDecl, ...ast]); -fs.writeFileSync(OUTPUT_PATH, output); -console.log(`Generated ${OUTPUT_PATH}`); diff --git a/src/inspect_scout/_view/www/src/App.tsx b/src/inspect_scout/_view/www/src/App.tsx deleted file mode 100644 index 4646d739c..000000000 --- a/src/inspect_scout/_view/www/src/App.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { FC, createContext, useMemo } from "react"; -import { RouterProvider } from "react-router-dom"; - -import "prismjs"; -import "prismjs/components/prism-bash"; -import "prismjs/components/prism-clike"; -import "prismjs/components/prism-javascript"; -import "prismjs/components/prism-json"; -import "prismjs/components/prism-python"; -import "prismjs/themes/prism.css"; -import "./app/App.css"; -import { useAppConfigAsync } from "./app/server/useAppConfig"; -import { useTopicInvalidation } from "./app/server/useTopicInvalidation"; -import { AppErrorBoundary } from "./AppErrorBoundary"; -import { createAppRouter } from "./AppRouter"; -import { ExtendedFindProvider } from "./components/ExtendedFindProvider"; - -export const AppModeContext = createContext("scans"); - -export interface AppProps { - mode?: "scans" | "workbench"; -} - -export const App: FC = (props) => { - const invalidationReady = useTopicInvalidation(); - - return invalidationReady ? : null; -}; - -const AppContent: FC = ({ mode = "scans" }) => { - const router = useAppRouter(mode); - - return router ? ( - - - - - - - - ) : null; -}; - -const useAppRouter = (mode: "scans" | "workbench") => { - const { data: appConfig } = useAppConfigAsync(); - return useMemo( - () => (appConfig ? createAppRouter({ mode, config: appConfig }) : null), - [mode, appConfig] - ); -}; diff --git a/src/inspect_scout/_view/www/src/AppErrorBoundary.tsx b/src/inspect_scout/_view/www/src/AppErrorBoundary.tsx deleted file mode 100644 index babbecc9d..000000000 --- a/src/inspect_scout/_view/www/src/AppErrorBoundary.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Component, ErrorInfo, ReactNode } from "react"; - -import { ErrorPanel } from "./components/ErrorPanel"; - -interface Props { - children: ReactNode; -} - -interface State { - hasError: boolean; - error?: Error; -} - -export class AppErrorBoundary extends Component { - constructor(props: Props) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError(error: Error): State { - // Update state so the next render will show the fallback UI. - return { hasError: true, error: error }; - } - - componentDidCatch(error: Error, errorInfo: ErrorInfo): void { - // You can also log the error to an error reporting service - console.log({ error, errorInfo }); - } - - render(): ReactNode { - if (this.state.hasError) { - console.error({ e: this.state.error }); - if (this.state.error) { - return ( - - ); - } else { - return ( -
An unknown error with no additional information occured.
- ); - } - } - return this.props.children; - } -} diff --git a/src/inspect_scout/_view/www/src/AppRouter.tsx b/src/inspect_scout/_view/www/src/AppRouter.tsx deleted file mode 100644 index a1e023931..000000000 --- a/src/inspect_scout/_view/www/src/AppRouter.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import { FC, useEffect } from "react"; -import { - createHashRouter, - Outlet, - useLocation, - useParams, -} from "react-router-dom"; - -import { ActivityBarLayout } from "./app/components/ActivityBarLayout"; -import { ProjectPanel } from "./app/project/ProjectPanel"; -import { RunScanPanel } from "./app/runScan/RunScanPanel"; -import { ScanPanel } from "./app/scan/ScanPanel"; -import { ScannerResultPanel } from "./app/scannerResult/ScannerResultPanel"; -import { ScansPanel } from "./app/scans/ScansPanel"; -import { useAppConfig } from "./app/server/useAppConfig"; -import { TranscriptPanel } from "./app/transcript/TranscriptPanel"; -import { TranscriptsPanel } from "./app/transcripts/TranscriptsPanel"; -import { ValidationPanel } from "./app/validation/ValidationPanel"; -import { FindBand } from "./components/FindBand"; -import { - LoggingNavigate, - useLoggingNavigate, -} from "./debugging/navigationDebugging"; -import { useWindowMessaging } from "./hooks/useWindowMessaging"; -import { - kScansRootRouteUrlPattern, - kScansRouteUrlPattern, - kScansWithPathRouteUrlPattern, - kScanRouteUrlPattern, - isValidScanPath, - parseScanParams, - kTranscriptsRouteUrlPattern, - kTranscriptDetailRoute, - kProjectRouteUrlPattern, - kValidationRouteUrlPattern, - scanResultRoute, - scanRoute, - scansRoute, -} from "./router/url"; -import { useStore } from "./state/store"; -import { AppConfig } from "./types/api-types"; - -export interface AppRouterConfig { - mode: "scans" | "workbench"; - config: AppConfig; -} - -// Creates a layout component that handles embedded state and tracks route changes -const createAppLayout = (routerConfig: AppRouterConfig) => { - const AppLayout = () => { - const showFind = useStore((state) => state.showFind); - const setShowFind = useStore((state) => state.setShowFind); - const singleFileMode = useStore((state) => state.singleFileMode); - const config = useAppConfig(); - - useFindBandShortcut(); - useWindowMessaging(); - useRoutingInitializer(config.scans.dir); - - const content = ; - return ( - <> - {showFind && ( - { - setShowFind(false); - }} - /> - )} - - {routerConfig.mode === "workbench" && !singleFileMode ? ( - {content} - ) : ( - content - )} - - ); - }; - - return AppLayout; -}; - -// Wrapper component that validates scan path before rendering -const ScanOrScanResultsRoute = () => { - const params = useParams<{ scansDir?: string; "*": string }>(); - const { scansDir, relativePath, scanResultUuid } = parseScanParams(params); - - // If there's a scan result UUID, render the ScanResultPanel - if (scanResultUuid) { - return ; - } - - // Validate that the path ends with the correct scan_id pattern - if (!isValidScanPath(relativePath)) { - // Redirect to /scans preserving the path structure - return ( - - ); - } - - return ; -}; - -const ProjectPanelRoute = () => { - const config = useAppConfig(); - return ; -}; - -export const createAppRouter = (config: AppRouterConfig) => { - const AppLayout = createAppLayout(config); - const transcriptsDir = config.config.transcripts; - - return createHashRouter( - [ - { - path: "/", - element: , - children: [ - { - index: true, - element: , - }, - { - path: kScansRootRouteUrlPattern, - element: , - }, - { - path: kScansRouteUrlPattern, - element: , - }, - { - path: kScansWithPathRouteUrlPattern, - element: , - }, - { - path: kScanRouteUrlPattern, - element: , - }, - { - path: kTranscriptsRouteUrlPattern, - element: , - }, - { - path: kProjectRouteUrlPattern, - element: , - }, - { - path: kValidationRouteUrlPattern, - element: , - }, - { - path: kTranscriptDetailRoute, - element: , - }, - { - path: "/run", - element: , - }, - ], - }, - { - path: "*", - element: , - }, - ], - { basename: "" } - ); -}; - -// Handles routing initialization on first load -const useRoutingInitializer = (serverScansDir: string | undefined) => { - const navigate = useLoggingNavigate("useRoutingInitializer"); - const hasInitializedRouting = useStore( - (state) => state.hasInitializedRouting - ); - const setHasInitializedRouting = useStore( - (state) => state.setHasInitializedRouting - ); - const displayedScanResult = useStore((state) => state.displayedScanResult); - const selectedScanLocation = useStore((state) => state.selectedScanLocation); - const userScansDir = useStore((state) => state.userScansDir); - - useEffect(() => { - if (hasInitializedRouting) { - return; - } - - const currentPath = window.location.hash.slice(1); - const isDefaultRoute = - currentPath === "/" || - currentPath === "/scans" || - currentPath === "" || - currentPath === "/transcripts"; - - const resolvedScansDir = userScansDir || serverScansDir; - if (isDefaultRoute && selectedScanLocation && resolvedScansDir) { - if (displayedScanResult) { - void navigate( - scanResultRoute( - resolvedScansDir, - selectedScanLocation, - displayedScanResult - ), - { replace: true } - ); - } else { - void navigate(scanRoute(resolvedScansDir, selectedScanLocation), { - replace: true, - }); - } - } - - setHasInitializedRouting(true); - }, [ - hasInitializedRouting, - selectedScanLocation, - displayedScanResult, - navigate, - setHasInitializedRouting, - serverScansDir, - userScansDir, - ]); -}; - -// Global keyboard shortcut to open FindBand -const useFindBandShortcut = () => { - const setShowFind = useStore((state) => state.setShowFind); - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if ((e.ctrlKey || e.metaKey) && e.key === "f") { - e.preventDefault(); - setShowFind(true); - } - }; - - document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("keydown", handleKeyDown); - }; - }, [setShowFind]); -}; - -// Guard against redirecting when a navigation is already in-flight -// (window.location updated but router state hasn't reconciled yet) -const RootIndexRedirect: FC<{ - transcriptsDir: AppConfig["transcripts"]; -}> = ({ transcriptsDir }) => { - const { pathname, search, hash } = useLocation(); - const routerPath = pathname + search + hash; - const hashPath = window.location.hash.slice(1) || "/"; - - return hashPath === routerPath ? ( - - ) : null; -}; diff --git a/src/inspect_scout/_view/www/src/api/api-scout-server.ts b/src/inspect_scout/_view/www/src/api/api-scout-server.ts deleted file mode 100644 index ee7be172b..000000000 --- a/src/inspect_scout/_view/www/src/api/api-scout-server.ts +++ /dev/null @@ -1,452 +0,0 @@ -import { decompress as decompressZstd } from "fzstd"; - -import { ScanResultInputData, Input, InputType } from "../app/types"; -import type { Condition, OrderByModel } from "../query"; -import { - ActiveScansResponse, - AppConfig, - CreateValidationSetRequest, - MessagesEventsResponse, - Pagination, - ProjectConfig, - ProjectConfigInput, - RawEncoding, - ScanJobConfig, - ScannersResponse, - ScansResponse, - Status, - Transcript, - TranscriptInfo, - TranscriptsResponse, - ValidationCase, - ValidationCaseRequest, -} from "../types/api-types"; -import { encodeBase64Url } from "../utils/base64url"; -import { asyncJsonParse } from "../utils/json-worker"; - -import { NoPersistence, ScoutApiV2, ScalarValue, TopicVersions } from "./api"; -import { resolveAttachments } from "./attachmentsHelpers"; -import { serverRequestApi } from "./request"; - -export type HeaderProvider = () => Promise>; - -type TopicUpdateCallback = (topVersions: TopicVersions) => void; - -const ENCODING_ZSTD: RawEncoding = "zstd"; - -/** - * Fetch messages-events JSON for a transcript. - * Requests raw zstd-compressed bytes and decompresses client-side. - * Falls back to uncompressed JSON if server doesn't support zstd. - */ -async function fetchMessagesEvents( - requestApi: ReturnType, - encodedDir: string, - encodedId: string -): Promise { - const url = `/transcripts/${encodedDir}/${encodedId}/messages-events`; - - const result = await requestApi.fetchBytes( - "GET", - url, - { "X-Accept-Raw-Encoding": ENCODING_ZSTD }, - // Server returns application/json when transcoding to deflate - "application/json" - ); - - const encoding = result.headers.get("X-Content-Encoding"); - - if (encoding === ENCODING_ZSTD) { - return new TextDecoder().decode( - decompressZstd(new Uint8Array(result.data)) - ); - } - - if (encoding !== null) { - throw new Error(`Unsupported X-Content-Encoding: ${encoding}`); - } - - // No X-Content-Encoding header means uncompressed (or browser-handled compression) - return new TextDecoder().decode(result.data); -} - -export const apiScoutServer = ( - options: { - apiBaseUrl?: string; - headerProvider?: HeaderProvider; - customFetch?: typeof fetch; - disableSSE?: boolean; - } = {} -): ScoutApiV2 => { - const { - apiBaseUrl = "/api/v2", - headerProvider, - customFetch, - disableSSE, - } = options; - const requestApi = serverRequestApi(apiBaseUrl, headerProvider, customFetch); - - return { - capability: "workbench", - getConfig: async (): Promise => { - const result = await requestApi.fetchString("GET", `/app-config`); - return asyncJsonParse(result.raw); - }, - getTranscripts: async ( - transcriptsDir: string, - filter?: Condition, - orderBy?: OrderByModel | OrderByModel[], - pagination?: Pagination - ): Promise => { - const result = await requestApi.fetchString( - "POST", - `/transcripts/${encodeBase64Url(transcriptsDir)}`, - {}, - JSON.stringify({ - filter: filter ?? null, - order_by: orderBy ?? null, - pagination: pagination ?? null, - }) - ); - - const parsedResult = await asyncJsonParse( - result.raw - ); - return parsedResult; - }, - hasTranscript: async ( - transcriptsDir: string, - id: string - ): Promise => { - try { - await requestApi.fetchVoid( - "HEAD", - `/transcripts/${encodeBase64Url(transcriptsDir)}/${encodeURIComponent(id)}/info` - ); - return true; - } catch (error) { - if (error instanceof Error && error.message.includes("404")) { - return false; - } - throw error; - } - }, - getTranscript: async ( - transcriptsDir: string, - id: string - ): Promise => { - const encodedDir = encodeBase64Url(transcriptsDir); - const encodedId = encodeURIComponent(id); - - const [infoResult, messagesEventsJson] = await Promise.all([ - requestApi.fetchString( - "GET", - `/transcripts/${encodedDir}/${encodedId}/info` - ), - fetchMessagesEvents(requestApi, encodedDir, encodedId), - ]); - - const [info, { messages, events, attachments }] = await Promise.all([ - asyncJsonParse(infoResult.raw), - asyncJsonParse(messagesEventsJson), - ]); - - return { - ...info, - ...(attachments && Object.keys(attachments).length > 0 - ? { - messages: resolveAttachments(messages, attachments), - events: resolveAttachments(events, attachments), - } - : { messages, events }), - }; - }, - getTranscriptsColumnValues: async ( - transcriptsDir: string, - column: string, - filter: Condition - ): Promise => { - const result = await requestApi.fetchString( - "POST", - `/transcripts/${encodeBase64Url(transcriptsDir)}/distinct`, - {}, - JSON.stringify({ column, filter: filter ?? null }) - ); - return asyncJsonParse(result.raw); - }, - getScan: async (scansDir: string, scanPath: string): Promise => { - const result = await requestApi.fetchString( - "GET", - `/scans/${encodeBase64Url(scansDir)}/${encodeBase64Url(scanPath)}` - ); - - return asyncJsonParse(result.raw); - }, - - getScans: async ( - scansDir: string, - filter?: Condition, - orderBy?: OrderByModel | OrderByModel[], - pagination?: Pagination - ): Promise => { - const result = await requestApi.fetchString( - "POST", - `/scans/${encodeBase64Url(scansDir)}`, - {}, - JSON.stringify({ - filter: filter ?? null, - order_by: orderBy ?? null, - pagination: pagination ?? null, - }) - ); - return asyncJsonParse(result.raw); - }, - getScansColumnValues: async ( - scansDir: string, - column: string, - filter: Condition | undefined - ): Promise => { - const result = await requestApi.fetchString( - "POST", - `/scans/${encodeBase64Url(scansDir)}/distinct`, - {}, - JSON.stringify({ column, filter: filter ?? null }) - ); - return asyncJsonParse(result.raw); - }, - getScannerDataframe: async ( - scansDir: string, - scanPath: string, - scanner: string - ): Promise => { - const result = await requestApi.fetchBytes( - "GET", - `/scans/${encodeBase64Url(scansDir)}/${encodeBase64Url(scanPath)}/${encodeURIComponent(scanner)}` - ); - return result.data; - }, - getScannerDataframeInput: async ( - scansDir: string, - scanPath: string, - scanner: string, - uuid: string - ): Promise => { - // Fetch the data - const response = await requestApi.fetchType( - "GET", - `/scans/${encodeBase64Url(scansDir)}/${encodeBase64Url(scanPath)}/${encodeURIComponent(scanner)}/${encodeURIComponent(uuid)}/input` - ); - const input = response.parsed; - - // Read header to determine the input type - const inputType = response.headers.get("X-Input-Type"); - if (!inputType) { - throw new Error("Missing input type from server"); - } - if ( - !["transcript", "message", "messages", "event", "events"].includes( - inputType - ) - ) { - throw new Error(`Unknown input type from server: ${inputType}`); - } - - // Return the DataFrameInput - return { input, inputType: inputType as InputType }; - }, - getActiveScans: async (): Promise => - asyncJsonParse( - (await requestApi.fetchString("GET", `/scans/active`)).raw - ), - postCode: async (condition: Condition): Promise> => - asyncJsonParse>( - ( - await requestApi.fetchString( - "POST", - `/code`, - {}, - JSON.stringify(condition) - ) - ).raw - ), - getProjectConfig: async (): Promise<{ - config: ProjectConfig; - etag: string; - }> => { - const response = await requestApi.fetchType( - "GET", - `/project/config` - ); - const etag = response.headers.get("ETag")?.replace(/"/g, "") ?? ""; - return { config: response.parsed, etag }; - }, - updateProjectConfig: async ( - config: ProjectConfigInput, - etag: string | null - ): Promise<{ config: ProjectConfig; etag: string }> => { - const headers: Record = {}; - if (etag) { - headers["If-Match"] = `"${etag}"`; - } - const response = await requestApi.fetchType( - "PUT", - `/project/config`, - { - headers, - body: JSON.stringify(config), - } - ); - const newEtag = response.headers.get("ETag")?.replace(/"/g, "") ?? ""; - return { config: response.parsed, etag: newEtag }; - }, - startScan: async (config: ScanJobConfig): Promise => - asyncJsonParse( - ( - await requestApi.fetchString( - "POST", - `/startscan`, - {}, - JSON.stringify(config) - ) - ).raw - ), - getScanners: async (): Promise => { - const result = await requestApi.fetchString("GET", `/scanners`); - return asyncJsonParse(result.raw); - }, - - // Validation API - getValidationSets: async (): Promise => { - const result = await requestApi.fetchString("GET", `/validations`); - return asyncJsonParse(result.raw); - }, - getValidationCases: async (uri: string): Promise => { - const result = await requestApi.fetchString( - "GET", - `/validations/${encodeBase64Url(uri)}` - ); - return asyncJsonParse(result.raw); - }, - getValidationCase: async ( - uri: string, - caseId: string - ): Promise => { - const result = await requestApi.fetchString( - "GET", - `/validations/${encodeBase64Url(uri)}/${encodeBase64Url(caseId)}` - ); - return asyncJsonParse(result.raw); - }, - createValidationSet: async ( - request: CreateValidationSetRequest - ): Promise => { - const result = await requestApi.fetchString( - "POST", - `/validations`, - {}, - JSON.stringify(request) - ); - return asyncJsonParse(result.raw); - }, - upsertValidationCase: async ( - uri: string, - caseId: string, - data: ValidationCaseRequest - ): Promise => { - const result = await requestApi.fetchString( - "POST", - `/validations/${encodeBase64Url(uri)}/${encodeBase64Url(caseId)}`, - {}, - JSON.stringify(data) - ); - return asyncJsonParse(result.raw); - }, - deleteValidationCase: async ( - uri: string, - caseId: string - ): Promise => { - await requestApi.fetchVoid( - "DELETE", - `/validations/${encodeBase64Url(uri)}/${encodeBase64Url(caseId)}` - ); - }, - deleteValidationSet: async (uri: string): Promise => { - await requestApi.fetchVoid( - "DELETE", - `/validations/${encodeBase64Url(uri)}` - ); - }, - renameValidationSet: async ( - uri: string, - newName: string - ): Promise => { - const result = await requestApi.fetchString( - "PUT", - `/validations/${encodeBase64Url(uri)}/rename`, - {}, - JSON.stringify({ name: newName }) - ); - return asyncJsonParse(result.raw); - }, - - connectTopicUpdates: ( - onUpdate: (topVersions: TopicVersions) => void - ): (() => void) => - disableSSE - ? connectTopicUpdatesViaPolling(apiBaseUrl, onUpdate, customFetch) - : connectTopicUpdatesViaSSE(apiBaseUrl, onUpdate), - storage: NoPersistence, - }; -}; - -const connectTopicUpdatesViaPolling = ( - apiBaseUrl: string, - onUpdate: TopicUpdateCallback, - customFetch?: typeof fetch -): (() => void) => { - const controller = new AbortController(); - - const poll = () => - (customFetch ?? fetch)(`${apiBaseUrl}/topics`, { - signal: controller.signal, - }) - .then((res) => res.json()) - .then(onUpdate) - .catch((error) => { - if (error.name !== "AbortError") { - console.error("Topic polling failed:", error); - } - }); - - void poll(); - const intervalId = setInterval(() => void poll(), 10000); - - return () => { - controller.abort(); - clearInterval(intervalId); - }; -}; - -const connectTopicUpdatesViaSSE = ( - apiBaseUrl: string, - onUpdate: TopicUpdateCallback -): (() => void) => { - let timeoutId: ReturnType | undefined; - let eventSource: EventSource | undefined; - - const connect = () => { - eventSource = new EventSource(`${apiBaseUrl}/topics/stream`); - eventSource.onmessage = (e) => - onUpdate(JSON.parse(e.data) as TopicVersions); - eventSource.onerror = () => { - eventSource?.close(); - timeoutId = setTimeout(connect, 5000); - }; - }; - - connect(); - return () => { - clearTimeout(timeoutId); - eventSource?.close(); - }; -}; diff --git a/src/inspect_scout/_view/www/src/api/api-vscode.ts b/src/inspect_scout/_view/www/src/api/api-vscode.ts deleted file mode 100644 index 823d13226..000000000 --- a/src/inspect_scout/_view/www/src/api/api-vscode.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * VS Code API implementation for protocol version 2+. - * Composes apiScoutServer (for HTTP calls via JSON-RPC proxy) with VS Code storage. - */ - -import { VSCodeApi } from "../utils/vscode"; - -import { ScoutApiV2 } from "./api"; -import { apiScoutServer } from "./api-scout-server"; -import { webViewJsonRpcClient } from "./jsonrpc"; -import { createJsonRpcFetch } from "./jsonrpc-fetch"; -import { createVSCodeStore } from "./vscode-storage"; - -export const apiVscode = (vscodeApi: VSCodeApi): ScoutApiV2 => { - const rpcClient = webViewJsonRpcClient(vscodeApi); - return { - ...apiScoutServer({ - customFetch: createJsonRpcFetch(rpcClient), - disableSSE: true, - }), - storage: createVSCodeStore(vscodeApi), - capability: "workbench", - }; -}; diff --git a/src/inspect_scout/_view/www/src/api/api.ts b/src/inspect_scout/_view/www/src/api/api.ts deleted file mode 100644 index 5b2edcf5b..000000000 --- a/src/inspect_scout/_view/www/src/api/api.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { StateStorage } from "zustand/middleware"; - -import { ScanResultInputData } from "../app/types"; -import type { Condition, OrderByModel } from "../query"; -import { - ActiveScansResponse, - AppConfig, - CreateValidationSetRequest, - InvalidationTopic, - Pagination, - ProjectConfig, - ProjectConfigInput, - ScanJobConfig, - ScannersResponse, - ScansResponse, - Status, - Transcript, - TranscriptsResponse, - ValidationCase, - ValidationCaseRequest, -} from "../types/api-types"; - -export type ClientStorage = StateStorage; - -export type ScalarValue = string | number | boolean | null; - -/** Topic versions: maps topic name to timestamp. */ -export type TopicVersions = Record; - -export interface ScoutApiV2 { - getConfig(): Promise; - getTranscripts( - transcriptsDir: string, - filter?: Condition, - orderBy?: OrderByModel | OrderByModel[], - pagination?: Pagination - ): Promise; - hasTranscript(transcriptsDir: string, id: string): Promise; - getTranscript(transcriptsDir: string, id: string): Promise; - getTranscriptsColumnValues( - transcriptsDir: string, - column: string, - filter: Condition | undefined - ): Promise; - getScans( - scansDir: string, - filter?: Condition, - orderBy?: OrderByModel | OrderByModel[], - pagination?: Pagination - ): Promise; - getScansColumnValues( - scansDir: string, - column: string, - filter: Condition | undefined - ): Promise; - getScan(scansDir: string, scanPath: string): Promise; - getScannerDataframe( - scansDir: string, - scanPath: string, - scanner: string - ): Promise; - getScannerDataframeInput( - scansDir: string, - scanPath: string, - scanner: string, - uuid: string - ): Promise; - getActiveScans(): Promise; - postCode(condition: Condition): Promise>; - getProjectConfig(): Promise<{ config: ProjectConfig; etag: string }>; - updateProjectConfig( - config: ProjectConfigInput, - etag: string | null - ): Promise<{ config: ProjectConfig; etag: string }>; - startScan(config: ScanJobConfig): Promise; - getScanners(): Promise; - connectTopicUpdates( - onUpdate: (topVersions: TopicVersions) => void - ): () => void; - - // Validation API - getValidationSets(): Promise; - getValidationCases(uri: string): Promise; - getValidationCase(uri: string, caseId: string): Promise; - createValidationSet(request: CreateValidationSetRequest): Promise; - upsertValidationCase( - uri: string, - caseId: string, - data: ValidationCaseRequest - ): Promise; - deleteValidationCase(uri: string, caseId: string): Promise; - deleteValidationSet(uri: string): Promise; - renameValidationSet(uri: string, newName: string): Promise; - - storage: ClientStorage; - capability: "scans" | "workbench"; -} - -export const NoPersistence: ClientStorage = { - getItem: (_name: string): string | null => { - return null; - }, - setItem: (_name: string, _value: string): void => {}, - removeItem: (_name: string): void => {}, -}; diff --git a/src/inspect_scout/_view/www/src/api/attachmentsHelpers.test.ts b/src/inspect_scout/_view/www/src/api/attachmentsHelpers.test.ts deleted file mode 100644 index 3228e2b8a..000000000 --- a/src/inspect_scout/_view/www/src/api/attachmentsHelpers.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { resolveAttachments } from "./attachmentsHelpers"; - -const attachments: Record = { - "00000000000000000000000000000001": "resolved_value_1", - "00000000000000000000000000000002": "resolved_value_2", - abcdef01234567890abcdef012345678: "another_resolved", -}; - -describe("resolveAttachments", () => { - describe("primitives", () => { - it.each([ - ["null", null, null], - ["undefined", undefined, undefined], - ["number", 42, 42], - ["boolean true", true, true], - ["boolean false", false, false], - ])("passes through %s unchanged", (_, input, expected) => { - expect(resolveAttachments(input, attachments)).toBe(expected); - }); - }); - - describe("strings", () => { - it.each([ - ["no attachment ref", "plain text", "plain text"], - ["partial match (short id)", "attachment://abc", "attachment://abc"], - [ - "single ref", - "attachment://00000000000000000000000000000001", - "resolved_value_1", - ], - [ - "ref with surrounding text", - "before attachment://00000000000000000000000000000001 after", - "before resolved_value_1 after", - ], - [ - "multiple refs", - "attachment://00000000000000000000000000000001 and attachment://00000000000000000000000000000002", - "resolved_value_1 and resolved_value_2", - ], - [ - "unknown ref unchanged", - "attachment://ffffffffffffffffffffffffffffffff", - "attachment://ffffffffffffffffffffffffffffffff", - ], - [ - "mixed known and unknown", - "attachment://00000000000000000000000000000001 attachment://ffffffffffffffffffffffffffffffff", - "resolved_value_1 attachment://ffffffffffffffffffffffffffffffff", - ], - ])("%s", (_, input, expected) => { - expect(resolveAttachments(input, attachments)).toBe(expected); - }); - }); - - describe("arrays", () => { - it("resolves strings in array", () => { - const input = [ - "attachment://00000000000000000000000000000001", - "plain", - "attachment://00000000000000000000000000000002", - ]; - expect(resolveAttachments(input, attachments)).toEqual([ - "resolved_value_1", - "plain", - "resolved_value_2", - ]); - }); - - it("preserves non-string elements", () => { - const input = [1, true, null, "text"]; - expect(resolveAttachments(input, attachments)).toEqual([ - 1, - true, - null, - "text", - ]); - }); - - it("handles nested arrays", () => { - const input = [["attachment://00000000000000000000000000000001"]]; - expect(resolveAttachments(input, attachments)).toEqual([ - ["resolved_value_1"], - ]); - }); - }); - - describe("objects", () => { - it("resolves string values", () => { - const input = { - key: "attachment://00000000000000000000000000000001", - }; - expect(resolveAttachments(input, attachments)).toEqual({ - key: "resolved_value_1", - }); - }); - - it("preserves non-string values", () => { - const input = { num: 42, bool: true, nil: null }; - expect(resolveAttachments(input, attachments)).toEqual({ - num: 42, - bool: true, - nil: null, - }); - }); - - it("handles nested objects", () => { - const input = { - outer: { - inner: "attachment://00000000000000000000000000000001", - }, - }; - expect(resolveAttachments(input, attachments)).toEqual({ - outer: { inner: "resolved_value_1" }, - }); - }); - - it("handles arrays inside objects", () => { - const input = { - items: ["attachment://00000000000000000000000000000001", "plain"], - }; - expect(resolveAttachments(input, attachments)).toEqual({ - items: ["resolved_value_1", "plain"], - }); - }); - - it("handles objects inside arrays", () => { - const input = [ - { text: "attachment://00000000000000000000000000000001" }, - { text: "plain" }, - ]; - expect(resolveAttachments(input, attachments)).toEqual([ - { text: "resolved_value_1" }, - { text: "plain" }, - ]); - }); - }); - - describe("deep nesting", () => { - it("resolves at arbitrary depth", () => { - const input = { - a: { - b: { - c: [ - { - d: "attachment://00000000000000000000000000000001", - }, - ], - }, - }, - }; - expect(resolveAttachments(input, attachments)).toEqual({ - a: { b: { c: [{ d: "resolved_value_1" }] } }, - }); - }); - }); - - describe("empty attachments", () => { - it("returns input unchanged when attachments empty", () => { - const input = { - text: "attachment://00000000000000000000000000000001", - }; - expect(resolveAttachments(input, {})).toEqual({ - text: "attachment://00000000000000000000000000000001", - }); - }); - }); - - describe("type preservation", () => { - it("preserves array type", () => { - const input = ["a", "b"]; - const result = resolveAttachments(input, attachments); - expect(Array.isArray(result)).toBe(true); - }); - - it("preserves object shape", () => { - const input = { a: 1, b: "text" }; - const result = resolveAttachments(input, attachments); - expect(Object.keys(result)).toEqual(["a", "b"]); - }); - }); -}); diff --git a/src/inspect_scout/_view/www/src/api/attachmentsHelpers.ts b/src/inspect_scout/_view/www/src/api/attachmentsHelpers.ts deleted file mode 100644 index 2d3edbd40..000000000 --- a/src/inspect_scout/_view/www/src/api/attachmentsHelpers.ts +++ /dev/null @@ -1,36 +0,0 @@ -const ATTACHMENT_PREFIX = "attachment://"; -const ATTACHMENT_PATTERN = /attachment:\/\/([a-f0-9]{32})/g; - -const resolveString = ( - text: string, - attachments: Record -): string => - text.includes(ATTACHMENT_PREFIX) - ? text.replace( - ATTACHMENT_PATTERN, - (match, id: string) => attachments[id] ?? match - ) - : text; - -const resolveAttachmentsImpl = ( - obj: unknown, - resolveFunc: (s: string) => string -): unknown => { - if (typeof obj === "string") return resolveFunc(obj); - if (typeof obj === "object" && obj !== null) { - if (Array.isArray(obj)) - return obj.map((item) => resolveAttachmentsImpl(item, resolveFunc)); - return Object.fromEntries( - Object.entries(obj).map(([k, v]) => [ - k, - resolveAttachmentsImpl(v, resolveFunc), - ]) - ); - } - return obj; -}; - -export const resolveAttachments = ( - obj: T, - attachments: Record -): T => resolveAttachmentsImpl(obj, (s) => resolveString(s, attachments)) as T; diff --git a/src/inspect_scout/_view/www/src/api/jsonrpc-fetch.test.ts b/src/inspect_scout/_view/www/src/api/jsonrpc-fetch.test.ts deleted file mode 100644 index 27283acbc..000000000 --- a/src/inspect_scout/_view/www/src/api/jsonrpc-fetch.test.ts +++ /dev/null @@ -1,298 +0,0 @@ -// @vitest-environment jsdom -import { describe, expect, it, vi } from "vitest"; - -import type { HttpProxyRequest, HttpProxyResponse } from "./jsonrpc-fetch"; -import { createJsonRpcFetch, kMethodHttpRequest } from "./jsonrpc-fetch"; - -function mockRpcClient(response: HttpProxyResponse) { - return vi.fn().mockResolvedValue(response); -} - -const validResponse: HttpProxyResponse = { - status: 200, - headers: { "content-type": "application/json" }, - body: '{"ok":true}', -}; - -describe("createJsonRpcFetch", () => { - describe("HTTP methods", () => { - it.each([ - [undefined, "GET"], - ["GET", "GET"], - ["get", "GET"], - ["POST", "POST"], - ["post", "POST"], - ["PUT", "PUT"], - ["put", "PUT"], - ["DELETE", "DELETE"], - ["delete", "DELETE"], - ] as const)("method %s becomes %s", async (inputMethod, expectedMethod) => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await fetch("/api/test", inputMethod ? { method: inputMethod } : {}); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.method).toBe(expectedMethod); - }); - - it("throws for unsupported HTTP method", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await expect(fetch("/api/test", { method: "PATCH" })).rejects.toThrow( - "Unsupported HTTP method: PATCH" - ); - }); - }); - - describe("URL/path extraction", () => { - it.each([ - ["/api/test", "/api/test"], - ["/api/test?foo=bar", "/api/test?foo=bar"], - ["/api/test?a=1&b=2", "/api/test?a=1&b=2"], - ["http://localhost/api/test", "/api/test"], - ["http://localhost/api/test?q=search", "/api/test?q=search"], - ])("extracts path from %s", async (input, expectedPath) => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await fetch(input); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.path).toBe(expectedPath); - }); - - it("handles URL object input", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await fetch(new URL("http://localhost/api/resource?id=123")); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.path).toBe("/api/resource?id=123"); - }); - - it("handles Request object input", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await fetch(new Request("http://localhost/api/from-request?x=1")); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.path).toBe("/api/from-request?x=1"); - }); - }); - - describe("headers conversion", () => { - it("converts Headers object", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - const headers = new Headers(); - headers.set("Authorization", "Bearer token"); - headers.set("Content-Type", "application/json"); - - await fetch("/api/test", { headers }); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.headers).toEqual({ - authorization: "Bearer token", - "content-type": "application/json", - }); - }); - - it("converts array of header tuples", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await fetch("/api/test", { - headers: [ - ["X-Custom", "value1"], - ["X-Other", "value2"], - ], - }); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.headers).toEqual({ - "X-Custom": "value1", - "X-Other": "value2", - }); - }); - - it("converts plain object headers", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await fetch("/api/test", { - headers: { "X-Api-Key": "secret", Accept: "application/json" }, - }); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.headers).toEqual({ - "X-Api-Key": "secret", - Accept: "application/json", - }); - }); - - it("handles no headers", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await fetch("/api/test"); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.headers).toEqual({}); - }); - }); - - describe("body handling", () => { - it("passes string body directly", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await fetch("/api/test", { method: "POST", body: '{"data":"value"}' }); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.body).toBe('{"data":"value"}'); - }); - - it("decodes ArrayBuffer body", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - const text = "binary data"; - const buffer = new ArrayBuffer(text.length); - const view = new Uint8Array(buffer); - for (let i = 0; i < text.length; i++) { - view[i] = text.charCodeAt(i); - } - - await fetch("/api/test", { method: "POST", body: buffer }); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.body).toBe("binary data"); - }); - - it("converts other body types via toString", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await fetch("/api/test", { method: "POST", body: 12345 as never }); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.body).toBe("12345"); - }); - - it("handles no body", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await fetch("/api/test"); - - const request = rpcClient.mock.calls[0]![1]![0] as HttpProxyRequest; - expect(request.body).toBeUndefined(); - }); - }); - - describe("RPC client invocation", () => { - it("calls rpcClient with correct method name", async () => { - const rpcClient = mockRpcClient(validResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await fetch("/api/test"); - - expect(rpcClient).toHaveBeenCalledWith(kMethodHttpRequest, [ - expect.any(Object), - ]); - }); - }); - - describe("response handling", () => { - it("returns Response with utf8 body", async () => { - const rpcClient = mockRpcClient({ - status: 200, - headers: { "content-type": "text/plain" }, - body: "Hello, World!", - bodyEncoding: "utf8", - }); - const fetch = createJsonRpcFetch(rpcClient); - - const response = await fetch("/api/test"); - - expect(response.status).toBe(200); - expect(response.headers.get("content-type")).toBe("text/plain"); - expect(await response.text()).toBe("Hello, World!"); - }); - - it("returns Response with base64-decoded body", async () => { - const rpcClient = mockRpcClient({ - status: 200, - headers: { "content-type": "application/octet-stream" }, - body: btoa("binary content"), - bodyEncoding: "base64", - }); - const fetch = createJsonRpcFetch(rpcClient); - - const response = await fetch("/api/test"); - - expect(response.status).toBe(200); - expect(await response.text()).toBe("binary content"); - }); - - it("returns Response with null body", async () => { - const rpcClient = mockRpcClient({ - status: 204, - headers: {}, - body: null, - }); - const fetch = createJsonRpcFetch(rpcClient); - - const response = await fetch("/api/test"); - - expect(response.status).toBe(204); - expect(await response.text()).toBe(""); - }); - - it("handles body without explicit encoding (defaults to utf8)", async () => { - const rpcClient = mockRpcClient({ - status: 200, - headers: {}, - body: "plain text", - }); - const fetch = createJsonRpcFetch(rpcClient); - - const response = await fetch("/api/test"); - - expect(await response.text()).toBe("plain text"); - }); - }); - - describe("error handling", () => { - const invalidResponses: [unknown, string][] = [ - [null, "null response"], - [undefined, "undefined response"], - ["string", "string response"], - [{}, "missing status"], - [{ status: 200 }, "missing headers"], - [{ status: 200, headers: {} }, "missing body"], - [{ status: "200", headers: {}, body: null }, "non-numeric status"], - [{ status: 200, headers: "invalid", body: null }, "non-object headers"], - [{ status: 200, headers: {}, body: 123 }, "non-string body"], - [ - { status: 200, headers: {}, body: "x", bodyEncoding: "invalid" }, - "invalid bodyEncoding", - ], - ]; - - it.each(invalidResponses)( - "throws for invalid response: %s", - async (invalidResponse) => { - const rpcClient = vi.fn().mockResolvedValue(invalidResponse); - const fetch = createJsonRpcFetch(rpcClient); - - await expect(fetch("/api/test")).rejects.toThrow( - "Invalid HTTP proxy response from extension host" - ); - } - ); - }); -}); diff --git a/src/inspect_scout/_view/www/src/api/jsonrpc-fetch.ts b/src/inspect_scout/_view/www/src/api/jsonrpc-fetch.ts deleted file mode 100644 index e8ed0503f..000000000 --- a/src/inspect_scout/_view/www/src/api/jsonrpc-fetch.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * HTTP proxy for VS Code webview environment. - * Routes fetch requests through JSON-RPC to the extension host. - */ - -import { JsonValue } from "../types/json-value"; - -import { JsonRpcParams, kMethodHttpRequest } from "./jsonrpc"; - -export { kMethodHttpRequest }; - -export type HttpProxyRequest = { - method: "GET" | "POST" | "PUT" | "DELETE"; - path: string; - headers?: Record; - body?: string; -}; - -export interface HttpProxyResponse { - status: number; - headers: Record; - body: string | null; - bodyEncoding?: "utf8" | "base64"; -} - -function isHttpProxyResponse(value: unknown): value is HttpProxyResponse { - if (typeof value !== "object" || value === null) return false; - if (!("status" in value) || typeof value.status !== "number") return false; - if ( - !("headers" in value) || - typeof value.headers !== "object" || - value.headers === null - ) - return false; - if ( - !("body" in value) || - (typeof value.body !== "string" && value.body !== null) - ) - return false; - if ( - "bodyEncoding" in value && - value.bodyEncoding !== "utf8" && - value.bodyEncoding !== "base64" - ) - return false; - return true; -} - -function toHttpMethod(method: string): HttpProxyRequest["method"] { - const upper = method.toUpperCase(); - if ( - upper === "GET" || - upper === "POST" || - upper === "PUT" || - upper === "DELETE" - ) { - return upper; - } - throw new Error(`Unsupported HTTP method: ${method}`); -} - -/** - * Creates a fetch function that proxies requests through JSON-RPC. - * Used in VS Code webview to route HTTP requests through the extension host. - */ -export function createJsonRpcFetch( - rpcClient: (method: string, params?: JsonRpcParams) => Promise -): typeof fetch { - return async ( - input: RequestInfo | URL, - init?: RequestInit - ): Promise => { - const url = - typeof input === "string" - ? input - : input instanceof Request - ? input.url - : input.toString(); - const urlObj = new URL(url, window.location.origin); - const path = urlObj.pathname + urlObj.search; - - const method = toHttpMethod(init?.method ?? "GET"); - - // Convert Headers to Record - const headers: Record = {}; - if (init?.headers) { - const headerEntries = - init.headers instanceof Headers - ? init.headers.entries() - : Array.isArray(init.headers) - ? init.headers - : Object.entries(init.headers); - for (const [key, value] of headerEntries) { - headers[key] = value; - } - } - - // Get body as string - let body: string | undefined; - if (init?.body) { - body = - typeof init.body === "string" - ? init.body - : init.body instanceof ArrayBuffer - ? new TextDecoder().decode(init.body) - : String(init.body); - } - - const request: HttpProxyRequest = { method, path, headers, body }; - const response = await rpcClient(kMethodHttpRequest, [request]); - if (!isHttpProxyResponse(response)) { - throw new Error("Invalid HTTP proxy response from extension host"); - } - - const responseBody: BodyInit | null = - response.body === null - ? null - : response.bodyEncoding === "base64" - ? Uint8Array.from(atob(response.body), (c) => c.charCodeAt(0)) - : response.body; - - return new Response(responseBody, { - status: response.status, - headers: new Headers(response.headers), - }); - }; -} diff --git a/src/inspect_scout/_view/www/src/api/jsonrpc.ts b/src/inspect_scout/_view/www/src/api/jsonrpc.ts deleted file mode 100644 index c0893cddc..000000000 --- a/src/inspect_scout/_view/www/src/api/jsonrpc.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { JsonArray, JsonObject, JsonValue } from "../types/json-value"; -import { VSCodeApi } from "../utils/vscode"; - -// Type definitions -export type JsonRpcParams = JsonArray | JsonObject; - -// This isn't strictly correct. The data field is spec'ed to be JsonValue, but since -// we're in control of the server, it's fine. -type JsonRpcErrorData = { description?: string } & { [key: string]: JsonValue }; - -export type JsonRpcClient = ( - method: string, - params?: JsonRpcParams -) => Promise; - -interface JsonRpcMessage { - jsonrpc: string; - id: number; -} - -interface JsonRpcRequest extends JsonRpcMessage { - method: string; - params?: JsonRpcParams; -} - -interface JsonRpcResponse extends JsonRpcMessage { - result?: JsonValue; - error?: JsonRpcError; -} - -interface JsonRpcError { - code: number; - message: string; - data?: JsonRpcErrorData; -} - -interface RequestHandlers { - resolve: (value: JsonValue) => void; - reject: (error: JsonRpcError) => void; -} - -// PostMessageTarget uses `unknown` because it's at the DOM boundary where -// MessageEvent.data is untyped. The JSON-RPC layer validates incoming data. -interface PostMessageTarget { - postMessage: (data: unknown) => void; - onMessage: (handler: (data: unknown) => void) => () => void; -} - -// Constants -export const kMethodEvalLogDir = "eval_log_dir"; -export const kMethodEvalLogs = "eval_logs"; -export const kMethodEvalLogFiles = "eval_log_files"; -export const kMethodEvalLog = "eval_log"; -export const kMethodEvalLogSize = "eval_log_size"; -export const kMethodEvalLogBytes = "eval_log_bytes"; -export const kMethodEvalLogHeaders = "eval_log_headers"; -export const kMethodPendingSamples = "eval_log_pending_samples"; -export const kMethodSampleData = "eval_log_sample_data"; -export const kMethodLogMessage = "log_message"; - -// Scout constants -export const kMethodGetScan = "get_scan"; -export const kMethodGetScans = "get_scans"; -export const kMethodGetScannerDataframe = "get_scanner_dataframe"; -export const kMethodGetScannerDataframeInput = "get_scanner_dataframe_input"; -export const kMethodHttpRequest = "http_request"; - -export const kJsonRpcParseError = -32700; -export const kJsonRpcInvalidRequest = -32600; -export const kJsonRpcMethodNotFound = -32601; -export const kJsonRpcInvalidParams = -32602; -export const kJsonRpcInternalError = -32603; -export const kJsonRpcVersion = "2.0"; - -export function webViewJsonRpcClient(vscode: VSCodeApi): JsonRpcClient { - const target: PostMessageTarget = { - postMessage: (data: unknown) => { - vscode.postMessage(data); - }, - onMessage: (handler: (data: unknown) => void) => { - const onMessage = (ev: MessageEvent) => { - handler(ev.data); - }; - window.addEventListener("message", onMessage); - return () => { - window.removeEventListener("message", onMessage); - }; - }, - }; - return jsonRpcPostMessageRequestTransport(target).request; -} - -const toErrorData = (data: unknown): JsonRpcErrorData => - typeof data === "string" - ? { description: data } - : typeof data === "object" && data !== null - ? (data as JsonRpcErrorData) - : { description: JSON.stringify(data) }; - -export function jsonRpcError( - message: string, - data?: unknown, - code?: number -): JsonRpcError { - return { - code: code || -3200, - message, - data: data !== undefined ? toErrorData(data) : undefined, - }; -} - -export function asJsonRpcError(error: unknown): JsonRpcError { - if (typeof error === "object" && error !== null) { - const err = error as { message?: string; data?: unknown; code?: number }; - if (typeof err.message === "string") { - return jsonRpcError(err.message, err.data, err.code); - } - } - return jsonRpcError(String(error)); -} - -export function jsonRpcPostMessageRequestTransport(target: PostMessageTarget) { - const requests = new Map(); - const disconnect = target.onMessage((ev: unknown) => { - const response = asJsonRpcResponse(ev); - if (response) { - const request = requests.get(response.id); - if (request) { - requests.delete(response.id); - if (response.error) { - request.reject(response.error); - } else { - request.resolve(response.result ?? null); - } - } - } - }); - - return { - request: (method: string, params?: JsonRpcParams): Promise => { - return new Promise((resolve, reject) => { - const requestId = Math.floor(Math.random() * 1e6); - requests.set(requestId, { resolve, reject }); - const request: JsonRpcRequest = { - jsonrpc: kJsonRpcVersion, - id: requestId, - method, - params, - }; - target.postMessage(request); - }); - }, - disconnect, - }; -} - -export function jsonRpcPostMessageServer( - target: PostMessageTarget, - methods: - | { [key: string]: (params: unknown) => Promise } - | ((name: string) => ((params: unknown) => Promise) | undefined) -): () => void { - const lookupMethod = - typeof methods === "function" ? methods : (name: string) => methods[name]; - - return target.onMessage((data: unknown) => { - const request = asJsonRpcRequest(data); - if (request) { - const method = lookupMethod(request.method); - if (!method) { - target.postMessage(methodNotFoundResponse(request)); - return; - } - - method(request.params || []) - .then((value) => { - target.postMessage(jsonRpcResponse(request, value)); - }) - .catch((error: unknown) => { - target.postMessage({ - jsonrpc: request.jsonrpc, - id: request.id, - error: asJsonRpcError(error), - }); - }); - } - }); -} - -function isJsonRpcMessage(message: unknown): message is JsonRpcMessage { - return ( - typeof message === "object" && - message !== null && - "jsonrpc" in message && - "id" in message - ); -} - -function isJsonRpcRequest(message: JsonRpcMessage): message is JsonRpcRequest { - return (message as JsonRpcRequest).method !== undefined; -} - -function asJsonRpcMessage(data: unknown): JsonRpcMessage | null { - if (isJsonRpcMessage(data) && data.jsonrpc === kJsonRpcVersion) { - return data; - } - return null; -} - -function asJsonRpcRequest(data: unknown): JsonRpcRequest | null { - const message = asJsonRpcMessage(data); - if (message && isJsonRpcRequest(message)) { - return message; - } - return null; -} - -function asJsonRpcResponse(data: unknown): JsonRpcResponse | null { - const message = asJsonRpcMessage(data); - if (message) { - return message as JsonRpcResponse; - } - return null; -} - -function jsonRpcResponse( - request: JsonRpcRequest, - result: JsonValue -): JsonRpcResponse { - return { - jsonrpc: request.jsonrpc, - id: request.id, - result, - }; -} - -function jsonRpcErrorResponse( - request: JsonRpcRequest, - code: number, - message: string -): JsonRpcResponse { - return { - jsonrpc: request.jsonrpc, - id: request.id, - error: jsonRpcError(message, undefined, code), - }; -} - -function methodNotFoundResponse(request: JsonRpcRequest): JsonRpcResponse { - return jsonRpcErrorResponse( - request, - kJsonRpcMethodNotFound, - `Method '${request.method}' not found.` - ); -} diff --git a/src/inspect_scout/_view/www/src/api/request.ts b/src/inspect_scout/_view/www/src/api/request.ts deleted file mode 100644 index a22ca71f1..000000000 --- a/src/inspect_scout/_view/www/src/api/request.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { asyncJsonParse } from "../utils/json-worker"; - -type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD"; - -export class ApiError extends Error { - status: number; - constructor(status: number, message: string) { - super(message); - this.status = status; - } -} - -export interface Request { - headers?: Record; - body?: string; - parse?: (text: string) => Promise; - handleError?: (status: number) => T | undefined; - /** - * Opt into browser caching. Only use for endpoints that set appropriate caching - * headers (e.g., ETag). Caching is disabled by default. - */ - enableBrowserCache?: boolean; -} - -export type HeaderProvider = () => Promise>; - -export interface ServerRequestApi { - fetchString: ( - method: HttpMethod, - path: string, - headers?: Record, - body?: string - ) => Promise<{ raw: string }>; - fetchVoid: ( - method: HttpMethod, - path: string, - headers?: Record - ) => Promise; - fetchBytes: ( - method: HttpMethod, - path: string, - headers?: Record, - extraAcceptType?: string - ) => Promise<{ data: ArrayBuffer; headers: Headers }>; - fetchType: ( - method: HttpMethod, - path: string, - request?: Request - ) => Promise<{ raw: string; parsed: T; headers: Headers }>; -} - -export function serverRequestApi( - baseUrl?: string, - getHeaders?: HeaderProvider, - customFetch?: typeof fetch -): ServerRequestApi { - const fetchFn = customFetch ?? fetch; - const apiUrl = baseUrl || ""; - - function buildApiUrl(path: string): string { - if (!apiUrl) { - return path; - } - const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl; - const cleanPath = path.startsWith("/") ? path : `/${path}`; - return base + cleanPath; - } - - function isApiCrossOrigin(): boolean { - try { - return Boolean( - apiUrl && new URL(apiUrl).origin !== window.location.origin - ); - } catch (error) { - return false; - } - } - - const withGlobalHeaders = async ( - headers: Record - ): Promise> => - // NOTE: It's typically not safe to use object spreading for headers since, - // in the general case, they could be HeadersInit which is more complex than - // Record. We're in complete control, so we'll just spread. - getHeaders ? { ...(await getHeaders()), ...headers } : headers; - - const fetchType = async ( - method: HttpMethod, - path: string, - request?: Request - ): Promise<{ raw: string; parsed: T; headers: Headers }> => { - const url = buildApiUrl(path); - - // By default, disable browser caching. When enableBrowserCache is true, - // omit these headers to let the browser handle ETag-based caching. - const baseHeaders = request?.enableBrowserCache - ? { - Accept: "application/json", - ...request?.headers, - } - : { - Accept: "application/json", - Pragma: "no-cache", - Expires: "0", - "Cache-Control": "no-cache", - ...request?.headers, - }; - - if (request?.body) { - baseHeaders["Content-Type"] = "application/json"; - } - - const headers = await withGlobalHeaders(baseHeaders); - - const response = await fetchFn(url, { - method, - headers, - body: request?.body, - credentials: isApiCrossOrigin() ? "include" : "same-origin", - }); - - if (!response.ok) { - const errorResponse = request?.handleError?.(response.status); - if (errorResponse) { - return { - raw: response.statusText, - parsed: errorResponse, - headers: response.headers, - }; - } - - const message = (await response.text()) || response.statusText; - throw new ApiError( - response.status, - `API Error ${response.status}: ${message}` - ); - } - - const text = await response.text(); - const parse = request?.parse || asyncJsonParse; - return { - parsed: (await parse(text)) as T, - raw: text, - headers: response.headers, - }; - }; - - const fetchString = async ( - method: HttpMethod, - path: string, - customHeaders?: Record, - body?: string - ): Promise<{ raw: string }> => { - const url = buildApiUrl(path); - - const baseHeaders = { - Accept: "application/json", - Pragma: "no-cache", - Expires: "0", - "Cache-Control": "no-cache", - ...customHeaders, - }; - - if (body) { - baseHeaders["Content-Type"] = "application/json"; - } - - const headers = await withGlobalHeaders(baseHeaders); - - const response = await fetchFn(url, { - method, - headers, - body, - credentials: isApiCrossOrigin() ? "include" : "same-origin", - }); - - if (response.ok) { - return { raw: await response.text() }; - } - - const message = (await response.text()) || response.statusText; - throw new ApiError(response.status, `HTTP ${response.status}: ${message}`); - }; - - /** - * Fetch from an endpoint that returns no body (e.g., 204 No Content). - * Expects a 2xx response with no meaningful body. - */ - const fetchVoid = async ( - method: HttpMethod, - path: string, - customHeaders?: Record - ): Promise => { - const url = buildApiUrl(path); - - const baseHeaders = { - Pragma: "no-cache", - Expires: "0", - "Cache-Control": "no-cache", - ...customHeaders, - }; - - const headers = await withGlobalHeaders(baseHeaders); - - const response = await fetchFn(url, { - method, - headers, - credentials: isApiCrossOrigin() ? "include" : "same-origin", - }); - - if (!response.ok) { - const message = (await response.text()) || response.statusText; - throw new ApiError( - response.status, - `HTTP ${response.status}: ${message}` - ); - } - }; - - /** - * Fetch binary data from an endpoint. - * - * @param extraAcceptType - Additional MIME type to include in Accept header. - * Use when an endpoint may return different content types depending on - * request headers or server state. For example, an endpoint that returns - * application/octet-stream when the client requests raw compressed bytes, - * but falls back to application/json when transcoding to browser-compatible - * compression. - */ - const fetchBytes = async ( - method: HttpMethod, - path: string, - headers?: Record, - extraAcceptType?: string - ): Promise<{ data: ArrayBuffer; headers: Headers }> => { - const url = buildApiUrl(path); - const acceptTypes = extraAcceptType - ? `application/octet-stream, ${extraAcceptType}` - : "application/octet-stream"; - - const response = await fetchFn(url, { - method, - headers: await withGlobalHeaders({ - Accept: acceptTypes, - Pragma: "no-cache", - Expires: "0", - "Cache-Control": "no-cache", - ...headers, - }), - credentials: isApiCrossOrigin() ? "include" : "same-origin", - }); - - if (!response.ok) { - const message = (await response.text()) || response.statusText; - throw new ApiError( - response.status, - `HTTP ${response.status}: ${message}` - ); - } - - return { - data: await response.arrayBuffer(), - headers: response.headers, - }; - }; - - return { - fetchString, - fetchVoid, - fetchBytes, - fetchType, - }; -} diff --git a/src/inspect_scout/_view/www/src/api/vscode-storage.ts b/src/inspect_scout/_view/www/src/api/vscode-storage.ts deleted file mode 100644 index b70d20ee7..000000000 --- a/src/inspect_scout/_view/www/src/api/vscode-storage.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * VS Code storage adapter for ClientStorage interface. - */ - -import { VSCodeApi } from "../utils/vscode"; - -import { ClientStorage } from "./api"; - -export const createVSCodeStore = (api: VSCodeApi): ClientStorage => ({ - getItem: (key: string): string | null => { - const state = api.getState(); - if (state && typeof state === "object") { - const stateObj = state as Record; - return stateObj[key] || null; - } - return null; - }, - setItem: (key: string, value: string): void => { - const existingState = api.getState() || {}; - const stateObj = ( - typeof existingState === "object" ? existingState : {} - ) as Record; - stateObj[key] = value; - api.setState(stateObj); - }, - removeItem: (key: string): void => { - const existingState = api.getState(); - if (existingState && typeof existingState === "object") { - const stateObj = existingState as Record; - delete stateObj[key]; - api.setState(stateObj); - } - }, -}); diff --git a/src/inspect_scout/_view/www/src/app/App.css b/src/inspect_scout/_view/www/src/app/App.css deleted file mode 100644 index e9f98d163..000000000 --- a/src/inspect_scout/_view/www/src/app/App.css +++ /dev/null @@ -1,1412 +0,0 @@ -:root { - --bs-border-radius: 3px; - --bs-border-radius-lg: 4px; - --bs-popover-max-width: 50%; - --inspect-find-background: var(--bs-body-bg); - --inspect-find-foreground: var(--bs-body-color); - --inspect-input-background: var(--bs-body-bg); - --inspect-input-foreground: var(--bs-body-color); - --inspect-input-border: var(--bs-light-border-subtle); - --inspect-diff-add-color: #dafbe1; - --inspect-diff-remove-color: #ffebe9; - --inspect-inactive-selection-background: var( - --vscode-editor-inactiveSelectionBackground, - #d9d9d9 - ); - --inspect-active-selection-background: var( - --vscode-editor-selectionBackground, - #d7d4f0 - ); - --inspect-focus-border-color: #86b7fe; - --inspect-focus-border-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25); - --inspect-focus-border-gray-color: #808080; - --inspect-focus-border-gray-shadow: 0 0 0 0.25rem rgba(48, 48, 48, 0.25); - - /* Inspect Font Sizes */ - --inspect-font-size-title: 1.5rem; - --inspect-font-size-title-secondary: 1.3rem; - --inspect-font-size-largest: 1.2rem; - --inspect-font-size-larger: 1.1rem; - --inspect-font-size-large: 1rem; - --inspect-font-size-base: 0.9rem; - --inspect-font-size-small: 0.8rem; - --inspect-font-size-smaller: 0.8rem; - --inspect-font-size-smallest: 0.7rem; - --inspect-font-size-smallestest: 0.6rem; - - /* Inspect Glass */ - --inspect-glass-color: #000000; - --inspect-glass-opacity: 0.3; - - /* VS Code Light Modern theme defaults (for non-VS Code context) */ - /* Complete set of variables used by @vscode-elements/elements */ - /* These are overridden by actual VS Code theme variables when running in VS Code */ - - /* Core colors */ - --vscode-foreground: #3b3b3b; - --vscode-descriptionForeground: #717171; - --vscode-errorForeground: #f14c4c; - --vscode-icon-foreground: #424242; - --vscode-focusBorder: #0090f1; - --vscode-contrastBorder: transparent; - - /* Font settings */ - --vscode-font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, - sans-serif; - --vscode-font-size: 13px; - --vscode-font-weight: normal; - --vscode-editor-font-family: Menlo, Monaco, "Courier New", monospace; - --vscode-editor-font-size: 12px; - --vscode-editor-font-weight: normal; - - /* Editor colors */ - --vscode-editor-background: #ffffff; - --vscode-editor-foreground: #3b3b3b; - --vscode-editor-inlineValuesForeground: rgba(0, 0, 0, 0.5); - --vscode-editorGroup-border: #e7e7e7; - --vscode-editorWidget-border: #d4d4d4; - - /* Input colors */ - --vscode-input-background: #ffffff; - --vscode-input-foreground: #3b3b3b; - --vscode-input-border: #cecece; - --vscode-input-placeholderForeground: #767676; - --vscode-inputOption-activeBackground: rgba(0, 144, 241, 0.2); - --vscode-inputOption-activeBorder: #0090f1; - --vscode-inputOption-activeForeground: #000000; - - /* Input validation */ - --vscode-inputValidation-warningBackground: #f2dede; - --vscode-inputValidation-warningBorder: #be8800; - --vscode-inputValidation-errorBackground: #f2dede; - --vscode-inputValidation-errorBorder: #be1100; - - /* Settings controls (used by vscode-elements textfield, checkbox, dropdown) */ - --vscode-settings-textInputBackground: #ffffff; - --vscode-settings-textInputForeground: #3b3b3b; - --vscode-settings-textInputBorder: #cecece; - --vscode-settings-checkboxBackground: #ffffff; - --vscode-settings-checkboxForeground: #3b3b3b; - --vscode-settings-checkboxBorder: #919191; - --vscode-settings-dropdownBackground: #ffffff; - --vscode-settings-dropdownForeground: #3b3b3b; - --vscode-settings-dropdownBorder: #cecece; - --vscode-settings-dropdownListBorder: #c8c8c8; - --vscode-settings-headerBorder: rgba(128, 128, 128, 0.35); - - /* Checkbox colors */ - --vscode-checkbox-background: #ffffff; - --vscode-checkbox-foreground: #3b3b3b; - --vscode-checkbox-border: #919191; - - /* Button colors */ - --vscode-button-background: #007acc; - --vscode-button-foreground: #ffffff; - --vscode-button-hoverBackground: #0062a3; - --vscode-button-border: transparent; - --vscode-button-separator: rgba(255, 255, 255, 0.4); - --vscode-button-secondaryBackground: #5f6a79; - --vscode-button-secondaryForeground: #ffffff; - --vscode-button-secondaryHoverBackground: #4c5561; - - /* Widget/shadow colors */ - --vscode-widget-shadow: rgba(0, 0, 0, 0.16); - --vscode-widget-border: #d4d4d4; - - /* Toolbar */ - --vscode-toolbar-hoverBackground: rgba(184, 184, 184, 0.31); - --vscode-toolbar-activeBackground: rgba(166, 166, 166, 0.31); - --vscode-toolbar-hoverOutline: transparent; - - /* List colors (used by select/dropdown components) */ - --vscode-list-hoverBackground: #e8e8e8; - --vscode-list-hoverForeground: #3b3b3b; - --vscode-list-activeSelectionBackground: #0060c0; - --vscode-list-activeSelectionForeground: #ffffff; - --vscode-list-activeSelectionIconForeground: #ffffff; - --vscode-list-inactiveSelectionBackground: #e4e6f1; - --vscode-list-focusOutline: #0090f1; - --vscode-list-focusAndSelectionOutline: #0090f1; - --vscode-list-focusHighlightForeground: #0066bf; - --vscode-list-highlightForeground: #0066bf; - - /* Menu colors */ - --vscode-menu-background: #ffffff; - --vscode-menu-foreground: #3b3b3b; - --vscode-menu-border: #d4d4d4; - --vscode-menu-selectionBackground: #0060c0; - --vscode-menu-selectionForeground: #ffffff; - --vscode-menu-selectionBorder: transparent; - --vscode-menu-separatorBackground: #d4d4d4; - - /* Badge colors */ - --vscode-badge-background: #007acc; - --vscode-badge-foreground: #ffffff; - --vscode-activityBarBadge-background: #007acc; - --vscode-activityBarBadge-foreground: #ffffff; - - /* Panel colors */ - --vscode-panel-background: #ffffff; - --vscode-panelTitle-activeBorder: #007acc; - --vscode-panelTitle-activeForeground: #3b3b3b; - --vscode-panelTitle-inactiveForeground: #717171; - - /* Sidebar colors */ - --vscode-sideBar-background: #f3f3f3; - --vscode-sideBarSectionHeader-background: #f3f3f3; - --vscode-sideBarTitle-foreground: #3b3b3b; - - /* Progress bar */ - --vscode-progressBar-background: #007acc; - - /* Scrollbar colors */ - --vscode-scrollbar-shadow: rgba(0, 0, 0, 0.16); - --vscode-scrollbarSlider-background: rgba(100, 100, 100, 0.4); - --vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7); - --vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6); - - /* Sash (resizable borders) */ - --vscode-sash-hoverBorder: #0090f1; - - /* Tree colors */ - --vscode-tree-indentGuidesStroke: #d4d4d4; - --vscode-tree-inactiveIndentGuidesStroke: rgba(212, 212, 212, 0.5); - - /* Keybinding table */ - --vscode-keybindingTable-headerBackground: rgba(128, 128, 128, 0.1); - --vscode-keybindingTable-rowsBackground: rgba(128, 128, 128, 0.04); -} - -body:not([class^="vscode-"]) button { - --bs-nav-pills-link-active-bg: #e3eaf1; - --bs-nav-pills-link-active-color: black; - --bs-nav-link-color: black; -} - -/* vscode-elements form component adjustments */ -vscode-label { - margin-bottom: 0; -} - -vscode-form-helper { - font-size: 12px; - margin-top: 0; - margin-bottom: 6px; -} - -/* AG Grid Theming */ -body { - --ag-background-color: var(--bs-body-bg); - --ag-foreground-color: var(--bs-body-color); - --ag-header-background-color: var(--bs-light-bg-subtle); - --ag-header-font-size: var(--inspect-font-size-small); - --ag-header-font-weight: 500; - --ag-header-text-color: var(--bs-secondary); - --ag-font-family: var(--bs-body-font-family); - --ag-wrapper-border: none; - --ag-row-hover-color: var(--bs-secondary-bg-subtle); - --ag-selected-row-background-color: var(--bs-secondary-bg-subtle); - --ag-border-radius: var(--bs-border-radius); -} - -.ag-cell-auto-height { - --reduceLineHeightBy: 16px; - line-height: calc( - var(--ag-internal-calculated-line-height) - var(--reduceLineHeightBy) - ); - padding-top: calc(var(--reduceLineHeightBy) / 2); - padding-bottom: calc(var(--reduceLineHeightBy) / 2); -} - -#app { - height: 100vh; - overflow-y: hidden; -} - -.app-main-grid { - display: grid; - height: 100vh; - overflow-y: hidden; - grid-template-rows: max-content max-content 1fr; -} - -a { - color: var(--bs-link-color); -} - -a:hover { - color: var(--bs-link-hover-color); -} - -.modal { - --bs-modal-margin: 0.25rem; -} - -.modal-backdrop { - --bs-backdrop-opacity: 0.4; -} - -.app-main-grid.single-file-mode { - grid-template-rows: max-content max-content 1fr; -} - -/* Inspect Text Styles */ -.text-style-label { - text-transform: uppercase !important; -} - -.text-style-secondary { - color: var(--bs-secondary) !important; -} - -.text-style-tertiary { - color: var(--bs-tertiary-color) !important; -} - -/* Inspect Font Size Styles */ -.text-size-title { - font-size: var(--inspect-font-size-title) !important; -} - -.text-size-title-secondary { - font-size: var(--inspect-font-size-title-secondary) !important; -} - -.text-size-largest { - font-size: var(--inspect-font-size-largest) !important; -} - -.text-size-larger { - font-size: var(--inspect-font-size-larger) !important; -} - -.text-size-large { - font-size: var(--inspect-font-size-large) !important; -} - -.text-size-base { - font-size: var(--inspect-font-size-base) !important; -} - -.text-size-small { - font-size: var(--inspect-font-size-small) !important; -} - -.text-size-smaller { - font-size: var(--inspect-font-size-smaller) !important; -} - -.text-size-smallest { - font-size: var(--inspect-font-size-smallest) !important; -} - -.text-size-smallestest { - font-size: var(--inspect-font-size-smallestest) !important; -} - -.text-truncate { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -} - -.three-line-clamp { - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - max-height: calc(3em * var(--bs-body-line-height)); -} - -body[class^="vscode-"] { - --bs-border-radius: 0; - --bs-border-radius-lg: 0; - --bs-body-bg: var(--vscode-editor-background); - --bs-secondary-bg-subtle: var(--vscode-editorHoverWidget-background); - --bs-primary-text-emphasis: var(--vscode-editorLink-activeForeground); - --bs-card-bg: var(--vscode-editor-background); - --bs-table-bg: var(--vscode-editor-background); - --bs-light-bg-subtle: var(--vscode-sideBar-background); - --bs-light-border-subtle: var(--vscode-sideBarSectionHeader-border); - --bs-body-color: var(--vscode-editor-foreground); - --bs-table-color: var(--vscode-editor-foreground); - --bs-accordion-btn-color: var(--vscode-editor-foreground); - --bs-emphasis-color: var(--vscode-editor-foreground); - --bs-navbar-brand-color: var(--vscode-editor-foreground); - --bs-navbar-brand-hover-color: var(--vscode-editor-foreground); - --bs-code-color: var(--vscode-editorInfo-foreground); - --bs-light: var(--vscode-sideBar-background); - --bs-btn-bg: var(--vscode-peekViewTitle-background); - --bs-primary: var(--vscode-banner-iconForeground); - --bs-primary-bg-subtle: var(--vscode-editor-selectionHighlightBackground); - --bs-nav-pills-link-active-bg: var(--vscode-banner-iconForeground); - --bs-link-color: var(--vscode-textLink-foreground); - --bs-link-hover-color: var(--vscode-textLink-activeForeground); - --bs-secondary: var(--vscode-breadcrumb-foreground); - --bs-secondary-bg: var(--vscode-list-inactiveSelectionBackground); - --bs-border-color: var(--vscode-editorGroup-border); - --bs-card-border-color: var(--vscode-editorGroup-border); - --bs-warning-bg-subtle: var(--vscode-inputValidation-warningBackground); - --bs-warning-text-emphasis: var(--vscode-input-foreground); - --bs-breadcrumb-divider-color: var(--vscode-foreground); - --inspect-find-background: var(--vscode-editorWidget-background); - --inspect-find-foreground: var(--vscode-editorWidget-foreground); - --inspect-input-background: var(--vscode-input-background); - --inspect-input-foreground: var(--vscode-input-foreground); - --inspect-input-border: var(--vscode-input-border); - --inspect-diff-add-color: var(--vscode-diffEditor-insertedTextBackground); - --inspect-diff-remove-color: var(--vscode-diffEditor-removedTextBackground); - --inspect-glass-color: var(--vscode-editor-foreground); - --inspect-glass-opacity: 0.15; -} - -html.vscode { - font-size: 13px; -} - -html.vscode .sample-input { - line-height: 1.3em; - -webkit-line-clamp: 4 !important; -} - -body[class^="vscode-"] .modal-backdrop { - --bs-backdrop-opacity: 0.15; - --bs-backdrop-bg: var(--vscode-editor-foreground); -} - -body[class^="vscode-"] code { - background-color: transparent; -} - -body[class^="vscode-"] .popover.rendered-content, -body[class^="vscode-"] .modal-dialog { - border: solid 1px var(--bs-border-color); -} - -body[class^="vscode-"] .popover-arrow::before { - border-left-color: var(--bs-border-color) !important; -} - -body[class^="vscode-"] - [data-popper-placement^="bottom"] - > .popper-arrow-container - > div, -body[class^="vscode-"] - [data-popper-placement^="left"] - > .popper-arrow-container - > div, -body[class^="vscode-"] - [data-popper-placement^="right"] - > .popper-arrow-container - > div { - border-color: transparent transparent var(--bs-card-border-color) !important; -} - -body[class^="vscode-"] .modal-content { - background-clip: unset; -} - -body[class^="vscode-"] .multi-score-label { - margin-bottom: 5px; -} - -body[class^="vscode-"] { - min-width: 400px; -} - -body[class^="vscode-"] .navbar-brand { - font-size: 1.1em; -} -body[class^="vscode-"] .navbar-brand > div { - margin-top: -0.2rem !important; -} - -body[class^="vscode-"] .task-title { - margin-top: 0.4em; -} - -body[class^="vscode-"] .task-model { - margin-top: 0.2rem; - font-size: 0.9rem; -} - -body[class^="vscode-"] .accordion-button::after { - --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='black'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); - background: white; - mask-image: var(--bs-accordion-btn-icon); -} - -.copy-button i.bi, -body[class^="vscode-"] .navbar-text i.bi { - color: var(--vscode-editor-foreground); -} - -body[class^="vscode-"] .btn-tools { - --bs-btn-hover-bg: var(--vscode-peekViewTitle-background); - --bs-btn-bg: var(--vscode-peekViewTitle-background); - --bs-btn-border-color: var(--vscode-peekViewTitle-background); - --bs-btn-hover-border-color: var(--vscode-peekViewTitle-background); - --bs-btn-color: var(--vscode-peekViewTitleDescription-foreground); - --bs-btn-hover-color: var(--vscode-peekViewTitleDescription-foreground); -} - -body[class^="vscode-"] .btn-primary { - --bs-btn-bg: var(--vscode-button-background); - --bs-btn-border-color: var(--vscode-button-background); - --bs-btn-hover-bg: var(--vscode-button-hoverBackground); - --bs-btn-hover-border-color: var(--vscode-button-hoverBackground); - --bs-btn-color: var(--vscode-button-foreground); - --bs-btn-hover-color: var(--vscode-button-foreground); - --bs-btn-disabled-color: var(--vscode-button-foreground); - --bs-btn-disabled-bg: var(--vscode-button-background); - --bs-btn-disabled-border-color: var(--vscode-button-background); - --bs-btn-disabled-opacity: 0.8; -} - -body[class^="vscode-"] .navbar-brand { - --bs-navbar-brand-color: var(--vscode-sideBarSectionHeader-foreground); - --bs-navbar-brand-hover-color: var(--vscode-sideBarSectionHeader-foreground); -} - -body[class^="vscode-"] .navbar-text { - --bs-navbar-color: var(--vscode-sideBarSectionHeader-foreground); -} - -body[class^="vscode-"] .accordion-item { - --bs-accordion-active-bg: var(--vscode-list-inactiveSelectionBackground); -} - -body[class^="vscode-"] .card-header { - --bs-border-color: var(--vscode-editorGroup-border); - --bs-card-border-color: var(--vscode-editorGroup-border); -} - -body[class^="vscode-"] .card { - --bs-border-color: var(--vscode-editorGroup-border); - --bs-card-border-color: var(--vscode-editorGroup-border); -} - -body[class^="vscode-"] .nav-pills { - --bs-nav-pills-link-active-bg: var(--vscode-notifications-background); - --bs-nav-pills-link-active-color: var(--vscode-notifications-foreground); -} - -body[class^="vscode-"] .nav-link { - --bs-nav-link-color: var(--vscode-notificationLink-foreground); - --bs-link-hover-color: var(--vscode-notificationLink-foreground); -} - -body[class^="vscode-"] .nav-link:hover { - --bs-nav-link-color: var(--vscode-notificationLink-foreground); - --bs-nav-link-hover-color: var(--vscode-notificationLink-foreground); - --bs-nav-tabs-link-hover-border-color: var( - --vscode-notificationLink-foreground - ); -} - -body[class^="vscode-"] .ansi-display { - --ansiBlack: var(--vscode-terminal-ansiBlack); - --ansiRed: var(--vscode-terminal-ansiRed); - --ansiGreen: var(--vscode-terminal-ansiGreen); - --ansiYellow: var(--vscode-terminal-ansiYellow); - --ansiBlue: var(--vscode-terminal-ansiBlue); - --ansiMagenta: var(--vscode-terminal-ansiMagenta); - --ansiCyan: var(--vscode-terminal-ansiCyan); - --ansiWhite: var(--vscode-terminal-ansiWhite); - --ansiBrightBlack: var(--vscode-terminal-ansiBrightBlack); - --ansiBrightRed: var(--vscode-terminal-ansiBrightRed); - --ansiBrightGreen: var(--vscode-terminal-ansiBrightGreen); - --ansiBrightYellow: var(--vscode-terminal-ansiBrightYellow); - --ansiBrightBlue: var(--vscode-terminal-ansiBrightBlue); - --ansiBrightMagenta: var(--vscode-terminal-ansiBrightMagenta); - --ansiBrightCyan: var(--vscode-terminal-ansiBrightCyan); - --ansiBrightWhite: var(--vscode-terminal-ansiBrightWhite); -} - -body[class^="vscode-"] .sidebar .list-group { - --bs-tertiary-bg: var(--vscode-list-hoverBackground); - --bs-secondary-color: var(--vscode-foreground); - --bs-list-group-active-bg: var(--vscode-sideBarSectionHeader-background); - --bs-list-group-active-border-color: var( - --vscode-sideBarSectionHeader-background - ); - --bs-list-group-action-active-bg: var( - --vscode-sideBarSectionHeader-background - ); - --bs-list-group-active-color: var(--vscode-sideBarSectionHeader-foreground); -} - -body[class^="vscode-"] .breadcrumb-item { - --bs-breadcrumb-divider-color: var(--vscode-foreground); -} - -body[class^="vscode-"] div.ap-control-bar .ap-fullscreen-button { - display: none; -} - -:root { - --bs-navbar-padding-y: 0; - --bs-navbar-brand-padding-y: 0; - --sidebar-width: 550px; -} - -body { - margin: 0; - padding: 0; -} - -.navbar { - padding-top: 0; - padding-bottom: 0; - background-color: var(--bs-light); - top: 0; -} - -.navbar-title-grid { - display: grid; - grid-template-columns: minmax(350px, 1fr) 1fr; - width: 100%; -} - -@media (max-width: 575px) { - .navbar .vertical-metric-label { - font-size: 0.7rem !important; - } -} - -@media (max-width: 575px) { - .tab-tools select { - width: 50px; - } -} - -@media (max-width: 575px) { - .navbar .vertical-metric-value { - margin-top: 0.2rem !important; - font-size: 0.9rem !important; - } -} - -@media (max-width: 575px) { - .navbar-title-grid { - grid-template-columns: 1fr auto-fill; - } -} - -[data-bs-theme="dark"] .navbar { - background-color: unset; -} - -.navbar-brand { - font-weight: 400; - font-size: 1.4em; -} - -.navbar-text { - padding-top: 0px; - padding-bottom: 0px; -} - -#sidebarToggle > i.bi { - font-size: 1.5em; -} - -.nav-link.active { - border-bottom-width: 0 !important; -} - -.workspace { - display: flex; - flex-direction: column; -} - -.workspace.full-screen { - top: 0; -} - -@media (min-width: 768px) { - .workspace { - left: var(--sidebar-width); - } - .workspace.full-screen { - left: 0; - } - .workspace.off-canvas { - left: 0; - } -} - -.no-last-para-padding > p:last-of-type { - margin-bottom: 0; -} - -.sidebar .list-group-item { - cursor: pointer; - border-left-width: none; - border-top: none; - border-right: none; - border-radius: 0; -} - -.btn-tools { - --bs-btn-bg: var(--bs-secondary-bg-subtle); - --bs-btn-hover-bg: var(--bs-secondary-bg-subtle); - --bs-btn-border-color: var(--bs-secondary-bg-subtle); - --bs-btn-hover-border-color: var(--bs-secondary-bg-subtle); -} - -.sidebar .list-group { - --bs-list-group-active-color: var(--bs-gray-700); - --bs-list-group-active-bg: var(--bs-gray-200); - --bs-list-group-active-border-color: var(--bs-gray-200); -} - -[data-bs-theme="dark"] .sidebar .list-group { - --bs-list-group-active-color: var(--bs-gray-300); - --bs-list-group-active-bg: var(--bs-gray-800); - --bs-list-group-active-border-color: var(--bs-gray-800); -} - -.markdown-content pre > code, -.markdown-content pre { - white-space: pre-wrap !important; - word-wrap: anywhere !important; -} - -.log pre code { - white-space: pre-wrap; - font-size: 0.9em; -} - -.log pre[class*="language-"] { - margin: 0; - padding: 0.3em; -} - -.log :not(pre) > code[class*="language-"], -.log pre[class*="language-"] { - background-color: var(--bs-body-background); -} - -.workspace pre[class*="language-"] { - margin: 0; - padding: 0.3em; -} - -.workspace :not(pre) > code[class*="language-"], -.workspace pre[class*="language-"] { - background-color: var(--bs-body-background); -} - -.tbd { - font-weight: 300; - opacity: 0.9; - width: 100%; - text-align: center; - padding-top: 10em; -} - -code:not(.sourceCode):not(.source-code):not([class^="language-"]) { - color: var(--bs-code-color); -} - -.token.attr-name, -.token.builtin, -.token.char, -.token.inserted, -.token.selector, -.token.string { - color: var(--bs-body-color); -} - -.token.operator { - background: inherit; -} - -pre[class*="language-"] { - border: unset; - border-radius: unset; - box-shadow: unset; -} - -.font-title { - font-size: 1.5rem; - font-weight: 600; -} - -.font-subtitle { - font-size: 1.1rem; - font-weight: 200; -} - -.tight-paragraphs p { - margin-bottom: 0; -} - -.tight-last-paragraph p:last-of-type { - margin-bottom: 0; -} - -.card { - margin-bottom: 0.5em; -} - -.card-header { - padding: 0.1em 1em 0.1em 1em; - font-size: 0.8rem; - font-weight: 500; -} - -.btn .btn-link { - cursor: pointer; -} - -[aria-expanded="false"] .hide-when-collapsed { - display: none; -} - -[aria-expanded="true"] .hide-when-expanded { - opacity: 0 !important; -} - -[aria-expanded="true"] .no-bottom-padding-when-expanded { - padding-bottom: 0 !important; -} - -[aria-expanded="true"] .zerowidth-when-expanded { - height: 0px !important; - width: 0px !important; - padding-left: 0 !important; - padding-right: 0 !important; - margin-left: 0 !important; - margin-right: 0 !important; -} - -[aria-expanded="true"] .zeroheight-when-expanded { - height: 0px !important; -} - -.accordion-item:not(.no-highlight) .accordion-button:not(.collapsed) { - border-left: solid var(--bs-accordion-active-bg) 2px; - border-right: solid var(--bs-accordion-active-bg) 2px; - border-top: solid var(--bs-accordion-active-bg) 2px; - background-color: var(--bs-body-background); - color: var(--bs-body-color); -} - -.accordion-item .accordion-button { - border-left: solid var(--bs-body-bg) 2px; - border-right: solid var(--bs-body-bg) 2px; - border-top: solid var(--bs-body-bg) 2px; - background-color: var(--bs-body-bg); - color: var(--bs-body-color); -} - -.accordion-button[aria-expanded="true"] .giant-text-when-expanded { - font-weight: 500; - font-size: 0.9rem !important; - flex-grow: 40; - padding-top: 0rem; - padding-bottom: 0rem; -} - -.accordion-button[aria-expanded="true"] .full-flex-basis-when-expanded { - flex-basis: content !important; - flex-shrink: 1; -} - -.markdown-content h1, -.markdown-content h2, -.markdown-content h3, -.markdown-content h4, -.markdown-content h5, -.markdown-content h6 { - font-size: 0.9em; - font-weight: 600; - margin-top: 0.25em; - margin-bottom: 0.25em; -} - -.sample-answer .markdown-content h1, -.sample-answer .markdown-content h2, -.sample-answer .markdown-content h3, -.sample-answer .markdown-content h4, -.sample-answer .markdown-content h5, -.sample-answer .markdown-content h6 { - font-size: 1em; - font-weight: 400; - margin-top: 0em; - margin-bottom: 0em; -} - -.accordion-item:not(.no-highlight) .collapse.show.highlight-when-expanded, -.accordion-item:not(.no-highlight) .collapsing.highlight-when-expanded, -.accordion-item:not(.no-highlight) - .accordion-button[aria-expanded="true"].highlight-when-expanded { - border-left: solid var(--bs-accordion-active-bg) 2px; - border-right: solid var(--bs-accordion-active-bg) 2px; - border-bottom: solid var(--bs-accordion-active-bg) 2px; -} - -.accordion-item.no-highlight .accordion-button { - background-color: var(--bs-body-bg); -} - -.accordion-button.toggle-rotated:after { - background-size: 0.8em; - height: 0.8em; - width: 0.8em; -} - -.accordion-header > :last-child::after, -.accordion-button:last-child:after { - color: white !important; -} - -.card .accordion-button.toggle-rotated:after { - margin-right: 1rem; -} - -.no-highlight, -.no-highlight:hover, -.no-highlight:active { - color: inherit; -} - -.toggle-rotated:before { - transition: transform 0.3s linear; -} - -[aria-expanded="false"] .toggle-rotated:before { - transform: none; -} - -[aria-expanded="true"] .toggle-rotated:before { - transform: rotate(90deg); -} - -.fadeout-when-not-collapsed:not(.collapsed) { - opacity: 0 !important; -} - -table.table { - width: unset; -} - -.table > :not(caption) > * > * { - background-color: unset; -} - -table.table.table-sm td { - padding: 0.1em; -} - -.popover.rendered-content { - display: flex; - flex-direction: column; - max-height: 80%; - max-width: 80%; - /** Do not define an overflow on the wrapper or your arrow to the element will disapear */ -} - -.popover.rendered-content .popover-header { - font-size: 0.8em; - font-weight: 400; -} - -.popover.rendered-content .popover-body { - overflow: auto; -} - -.modal-footer button { - padding: 0.2rem 0.5rem; - font-size: 0.8em; -} - -.do-not-collapse-self { - display: block; - height: auto !important; -} - -[data-tooltip] { - position: relative; -} -[data-tooltip]:hover::after { - content: attr(data-tooltip); - position: absolute; - line-height: 1.25; - background: var(--bs-light); - color: var(--bs-body-color); - opacity: 1; - padding: 4px 8px; - border-radius: 4px; - border: 1px solid var(--bs-border-color); - box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.25); - white-space: pre-wrap; - width: max-content; - max-width: 400px; - z-index: 1000; -} -[data-tooltip][data-tooltip-position="bottom-left"]:hover::after { - right: 0%; - top: 100%; -} - -/* ANSI Coloring */ -.ansi-display { - font-family: monospace; - white-space: pre-wrap; - --ansiBlack: #000000; - --ansiRed: #cd3131; - --ansiGreen: #00bc00; - --ansiYellow: #949800; - --ansiBlue: #0451a5; - --ansiMagenta: #bc05bc; - --ansiCyan: #0598bc; - --ansiWhite: #555555; - --ansiBrightBlack: #666666; - --ansiBrightRed: #cd3131; - --ansiBrightGreen: #14ce14; - --ansiBrightYellow: #b5ba00; - --ansiBrightBlue: #0451a5; - --ansiBrightMagenta: #bc05bc; - --ansiBrightCyan: #0598bc; - --ansiBrightWhite: #a5a5a5; -} - -@keyframes ansi-display-run-blink { - 50% { - opacity: 0; - } -} - -.tab-tools .form-select { - background-position: right 0.3rem center; - background-size: 10px 10px; - padding: 0.1rem 20px 0.1rem 10px; -} - -.tab-content > .active { - display: flex; -} - -.left-to-right-animate { - position: absolute; - animation: moveLeftToRight 2s linear infinite; -} - -@keyframes moveLeftToRight { - from { - margin-left: 0; - } - to { - margin-left: 95%; - } -} - -.expandable-panel pre { - overflow: unset; -} - -.markdown-content pre[class*="language-"], -pre[class*="language-"].tool-output, -.tool-output { - background-color: #f8f8f8; -} - -.vscode-dark .model-call pre[class*="language-"], -.vscode-dark .markdown-content pre[class*="language-"], -.vscode-dark pre[class*="language-"].tool-output, -.vscode-dark .tool-output { - background-color: #333333; -} - -.model-call pre[class*="language-"], -.markdown-content pre[class*="language-"], -pre[class*="language-"].tool-output { - border: none !important; - box-shadow: none !important; - border-radius: var(--bs-border-radius) !important; -} - -.vscode-dark pre.jsonPanel { - background: none !important; - border: none !important; - box-shadow: none !important; - border-radius: var(--bs-border-radius) !important; -} - -/* lightbox styles */ - -.lightbox-overlay .close-button, -.lightbox-overlay .nav-button { - /* Hide by default */ - opacity: 0; - pointer-events: none; /* so it doesn't register clicks when hidden */ - transition: opacity 0.3s ease; -} - -.lightbox-overlay:hover .close-button, -.lightbox-overlay .nav-button { - /* Show on hover */ - opacity: 1; - pointer-events: auto; -} - -/* jsondiffpatch */ - -.jsondiffpatch-delta { - padding: 1em; - background: var(--bs-light); - font-family: var(--bs-font-monospace); - font-size: 0.9em; -} -.jsondiffpatch-delta pre { - white-space: pre-wrap; - word-wrap: break-word; - word-break: break-all; - margin-bottom: 0; -} -ul.jsondiffpatch-delta { - list-style-type: none; - padding: 0 0 0 1.5em; - margin: 0; -} -.jsondiffpatch-delta ul { - list-style-type: none; - padding: 0 0 0 1.5em; - margin: 0; -} -.jsondiffpatch-added, -.jsondiffpatch-modified .jsondiffpatch-right-value pre, -.jsondiffpatch-textdiff-added { - background: var(--inspect-diff-add-color); -} - -.jsondiffpatch-deleted .jsondiffpatch-property-name, -.jsondiffpatch-deleted pre, -.jsondiffpatch-modified .jsondiffpatch-left-value pre, -.jsondiffpatch-textdiff-deleted { - background: var(--inspect-diff-remove-color); - text-decoration: line-through; -} -.jsondiffpatch-unchanged, -.jsondiffpatch-movedestination { - color: gray; -} -.jsondiffpatch-unchanged, -.jsondiffpatch-movedestination > .jsondiffpatch-value { - transition: all 0.5s; - -webkit-transition: all 0.5s; - overflow-y: hidden; -} -.jsondiffpatch-unchanged-showing .jsondiffpatch-unchanged, -.jsondiffpatch-unchanged-showing - .jsondiffpatch-movedestination - > .jsondiffpatch-value { - max-height: 100px; -} -.jsondiffpatch-unchanged-hidden .jsondiffpatch-unchanged, -.jsondiffpatch-unchanged-hidden - .jsondiffpatch-movedestination - > .jsondiffpatch-value { - max-height: 0; -} -.jsondiffpatch-unchanged-hiding - .jsondiffpatch-movedestination - > .jsondiffpatch-value, -.jsondiffpatch-unchanged-hidden - .jsondiffpatch-movedestination - > .jsondiffpatch-value { - display: block; -} -.jsondiffpatch-unchanged-visible .jsondiffpatch-unchanged, -.jsondiffpatch-unchanged-visible - .jsondiffpatch-movedestination - > .jsondiffpatch-value { - max-height: 100px; -} -.jsondiffpatch-unchanged-hiding .jsondiffpatch-unchanged, -.jsondiffpatch-unchanged-hiding - .jsondiffpatch-movedestination - > .jsondiffpatch-value { - max-height: 0; -} -.jsondiffpatch-unchanged-showing .jsondiffpatch-arrow, -.jsondiffpatch-unchanged-hiding .jsondiffpatch-arrow { - display: none; -} -.jsondiffpatch-value { - display: inline-block; -} -.jsondiffpatch-property-name { - display: inline-block; - padding-right: 5px; - vertical-align: top; -} -.jsondiffpatch-property-name:after { - content: ": "; -} -.jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after { - content: ": ["; -} -.jsondiffpatch-child-node-type-array:after { - content: "],"; -} -div.jsondiffpatch-child-node-type-array:before { - content: "["; -} -div.jsondiffpatch-child-node-type-array:after { - content: "]"; -} -.jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after { - content: ": {"; -} -.jsondiffpatch-child-node-type-object:after { - content: "},"; -} -div.jsondiffpatch-child-node-type-object:before { - content: "{"; -} -div.jsondiffpatch-child-node-type-object:after { - content: "}"; -} -.jsondiffpatch-value pre:after { - content: ","; -} -li:last-child > .jsondiffpatch-value pre:after, -.jsondiffpatch-modified > .jsondiffpatch-left-value pre:after { - content: ""; -} -.jsondiffpatch-modified .jsondiffpatch-value { - display: inline-block; -} -.jsondiffpatch-modified .jsondiffpatch-right-value { - margin-left: 0; -} -.jsondiffpatch-moved .jsondiffpatch-value { - display: none; -} -.jsondiffpatch-moved .jsondiffpatch-moved-destination { - display: inline-block; - background: #ffffbb; - color: #888; -} -.jsondiffpatch-moved .jsondiffpatch-moved-destination:before { - content: " => "; -} -ul.jsondiffpatch-textdiff { - padding: 0; -} -.jsondiffpatch-textdiff-location { - color: #bbb; - display: inline-block; - min-width: 60px; -} -.jsondiffpatch-textdiff-line { - display: inline-block; -} -.jsondiffpatch-textdiff-line-number:after { - content: ","; -} -.jsondiffpatch-error { - background: red; - color: white; - font-weight: bold; -} - -/* prism-custom.css */ -code[class*="language-"], -pre[class*="language-"] { - font-size: 0.7rem !important; -} - -.token { - font-size: 0.7rem !important; -} - -/* PrismJS 1.29.0 https://prismjs.com/download.html#themes=prism-dark&languages=markup+css+clike+javascript+bash+python */ -/* This has been generated from the URL above, then scoped within the - * .vscode-dark class. If it needs to be regenerated, be sure to add that back in. */ -.vscode-dark code[class*="language-"], -.vscode-dark pre[class*="language-"] { - color: #fff; - background: 0 0; - text-shadow: 0 -0.1em 0.2em #000; - font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; - font-size: 1em; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} -@media print { - .vscode-dark code[class*="language-"], - .vscode-dark pre[class*="language-"] { - text-shadow: none; - } -} -.vscode-dark :not(pre) > code[class*="language-"], -.vscode-dark pre[class*="language-"] { - background: #4c3f33; -} -.vscode-dark pre[class*="language-"] { - padding: 1em; - margin: 0.5em 0; - overflow: auto; - /* border: 0.3em solid #7a6651; */ - border-radius: 0.5em; - box-shadow: 1px 1px 0.5em #000 inset; -} -.vscode-dark :not(pre) > code[class*="language-"] { - padding: 0.15em 0.2em 0.05em; - border-radius: 0.3em; - /* border: 0.13em solid #7a6651; */ - box-shadow: 1px 1px 0.3em -0.1em #000 inset; - white-space: normal; -} -.vscode-dark .token.cdata, -.vscode-dark .token.comment, -.vscode-dark .token.doctype, -.vscode-dark .token.prolog { - color: #997f66; -} -.vscode-dark .token.punctuation { - opacity: 0.7; -} -.vscode-dark .token.namespace { - opacity: 0.7; -} -.vscode-dark .token.boolean, -.vscode-dark .token.constant, -.vscode-dark .token.number, -.vscode-dark .token.property, -.vscode-dark .token.symbol, -.vscode-dark .token.tag { - color: #d1939e; -} -.vscode-dark .token.attr-name, -.vscode-dark .token.builtin, -.vscode-dark .token.char, -.vscode-dark .token.inserted, -.vscode-dark .token.selector, -.vscode-dark .token.string { - color: #bce051; -} -.vscode-dark .language-css .token.string, -.vscode-dark .style .token.string, -.vscode-dark .token.entity, -.vscode-dark .token.operator, -.vscode-dark .token.url, -.vscode-dark .token.variable { - color: #f4b73d; -} -.vscode-dark .token.atrule, -.vscode-dark .token.attr-value, -.vscode-dark .token.keyword { - color: #d1939e; -} -.vscode-dark .token.important, -.vscode-dark .token.regex { - color: #e90; -} -.vscode-dark .token.bold, -.vscode-dark .token.important { - font-weight: 700; -} -.vscode-dark .token.italic { - font-style: italic; -} -.vscode-dark .token.entity { - cursor: help; -} -.vscode-dark .token.deleted { - color: red; -} -/* END PrismJS */ - -/* SVG Icon styles - following Bootstrap Icons pattern */ -.ii::before { - background-color: currentColor; -} - -.inspect-icon-16::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - mask: url("../img/inspect-16.svg") no-repeat center / contain; - -webkit-mask: url("../img/inspect-16.svg") no-repeat center / contain; -} - -.inspect-icon-back::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - mask: url("../img/inspect-back.svg") no-repeat center / contain; - -webkit-mask: url("../img/inspect-back.svg") no-repeat center / contain; -} - -.inspect-icon-forward::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - mask: url("../img/inspect-forward.svg") no-repeat center / contain; - -webkit-mask: url("../img/inspect-forward.svg") no-repeat center / contain; -} - -.inspect-icon-file::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - mask: url("../img/inspect-file.svg") no-repeat center / contain; - -webkit-mask: url("../img/inspect-file.svg") no-repeat center / contain; -} - -.inspect-icon-home::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - mask: url("../img/inspect-home.svg") no-repeat center / contain; - -webkit-mask: url("../img/inspect-home.svg") no-repeat center / contain; -} - -.inspect-icon-tasks::before { - content: ""; - width: 1em; - height: 1em; - display: inline-block; - vertical-align: middle; - margin-top: -2px; - mask: url("../img/tasks.svg") no-repeat center / contain; - -webkit-mask: url("../img/tasks.svg") no-repeat center / contain; -} - -/* Use this very carefully, find relies upon checking the -document selection to determine if it should skip a find match -so disabling selection can break that functionality */ -.hideSelection { - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; -} diff --git a/src/inspect_scout/_view/www/src/app/components/ActivityBar.module.css b/src/inspect_scout/_view/www/src/app/components/ActivityBar.module.css deleted file mode 100644 index 7ebb67861..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ActivityBar.module.css +++ /dev/null @@ -1,41 +0,0 @@ -.activityBar { - width: 78px; - border-right: solid 1px var(--bs-border-color); - background-color: var(--bs-light); -} - -.activityHost { - padding-top: 1rem; - padding-bottom: 1rem; - display: grid; - justify-content: center; - grid-auto-rows: max-content; - row-gap: 1rem; -} - -.activity { - display: flex; - flex-direction: column; - align-items: center; - flex-shrink: 1; - padding: 0.3rem; - border-radius: var(--bs-border-radius); -} - -.activity:hover { - background-color: var(--bs-body-bg); - cursor: pointer; -} - -.activity.selected { - background-color: var(--bs-primary-bg-subtle); -} - -.icon { - font-size: 1.1rem; -} - -.label { - font-size: 0.5rem; - text-transform: uppercase; -} diff --git a/src/inspect_scout/_view/www/src/app/components/ActivityBar.tsx b/src/inspect_scout/_view/www/src/app/components/ActivityBar.tsx deleted file mode 100644 index fdb652e97..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ActivityBar.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import styles from "./ActivityBar.module.css"; - -interface Activity { - id: string; - label: string; - icon: string; - description?: string; -} - -interface ActivityBarProps { - activities: Activity[]; - onSelectActivity: (id: string, options?: { openInNewTab?: boolean }) => void; - selectedActivity: string; -} - -export const ActivityBar: FC = ({ - activities, - selectedActivity, - onSelectActivity, -}) => { - return ( -
-
- {activities.map((activity) => ( - - ))} -
-
- ); -}; - -interface ActivityProps extends Activity { - selected?: boolean; - onSelect: (id: string, options?: { openInNewTab?: boolean }) => void; -} - -const Activity: FC = ({ - id, - label, - icon, - description, - selected, - onSelect, -}) => { - return ( -
onSelect(id, { openInNewTab: e.metaKey || e.ctrlKey })} - > - -
{label}
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/ActivityBarLayout.module.css b/src/inspect_scout/_view/www/src/app/components/ActivityBarLayout.module.css deleted file mode 100644 index af4ecaf07..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ActivityBarLayout.module.css +++ /dev/null @@ -1,23 +0,0 @@ -.outerLayout { - display: flex; - flex-direction: column; - height: 100vh; - width: 100vw; - overflow: hidden; -} - -.layout { - display: flex; - flex-direction: row; - flex: 1; - overflow: hidden; -} - -.content { - flex: 1; - overflow: hidden; -} - -.content.scrolling { - overflow: auto; -} diff --git a/src/inspect_scout/_view/www/src/app/components/ActivityBarLayout.tsx b/src/inspect_scout/_view/www/src/app/components/ActivityBarLayout.tsx deleted file mode 100644 index 54d0c0290..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ActivityBarLayout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { FC, ReactNode } from "react"; -import { useLocation } from "react-router-dom"; - -import { useLoggingNavigate } from "../../debugging/navigationDebugging"; -import { - activities, - getActivityById, - getActivityByRoute, -} from "../../router/activities"; -import { openRouteInNewTab } from "../../router/url"; -import { AppConfig } from "../../types/api-types"; - -import { ActivityBar } from "./ActivityBar"; -import styles from "./ActivityBarLayout.module.css"; -import { ProjectBar } from "./ProjectBar"; - -interface ActivityBarLayoutProps { - config: AppConfig; - children: ReactNode; -} - -/** - * Layout component that wraps all routes with the ActivityBar navigation - */ -export const ActivityBarLayout: FC = ({ - config, - children, -}) => { - const location = useLocation(); - const navigate = useLoggingNavigate("ActivityBarLayout"); - - // Determine the currently selected activity based on the current route - const currentActivity = getActivityByRoute(location.pathname); - const selectedActivityId = currentActivity?.id ?? ""; - - const handleSelectActivity = ( - activityId: string, - options?: { openInNewTab?: boolean } - ) => { - const activity = getActivityById(activityId); - if (activity) { - if (options?.openInNewTab) { - openRouteInNewTab(activity.route); - } else { - void navigate(activity.route); - } - } - }; - - return ( -
- -
- ({ - id: a.id, - label: a.label, - icon: a.icon, - description: a.description, - }))} - selectedActivity={selectedActivityId} - onSelectActivity={handleSelectActivity} - /> -
{children}
-
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/AddFilterButton.tsx b/src/inspect_scout/_view/www/src/app/components/AddFilterButton.tsx deleted file mode 100644 index b9e99e5ad..000000000 --- a/src/inspect_scout/_view/www/src/app/components/AddFilterButton.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { clsx } from "clsx"; -import { FC, useRef } from "react"; - -import { ScalarValue } from "../../api/api"; -import { ApplicationIcons } from "../../components/icons"; -import { PopOver } from "../../components/PopOver"; -import type { OperatorModel } from "../../query"; - -import { Chip } from "./Chip"; -import { - ColumnFilterEditor, - type AvailableColumn, -} from "./columnFilter/ColumnFilterEditor"; -import styles from "./FilterBar.module.css"; - -/** Props accepted by AddFilterButton - subset of useAddFilterPopover return */ -export interface AddFilterPopoverState { - isOpen: boolean; - setIsOpen: (open: boolean) => void; - selectedColumnId: string | null; - columns: AvailableColumn[]; - filterType: - | "string" - | "number" - | "date" - | "datetime" - | "boolean" - | "duration" - | "unknown"; - operator: OperatorModel; - setOperator: (op: OperatorModel) => void; - operatorOptions: OperatorModel[]; - value: string; - setValue: (v: string) => void; - value2: string; - setValue2: (v: string) => void; - isValueDisabled: boolean; - isRangeOperator: boolean; - handleColumnChange: (columnId: string) => void; - commitAndClose: () => void; - cancelAndClose: () => void; -} - -export interface AddFilterButtonProps { - /** Unique prefix for popover IDs */ - idPrefix: string; - /** Popover state from useAddFilterPopover hook */ - popoverState: AddFilterPopoverState; - /** Suggestions for autocomplete */ - suggestions?: ScalarValue[]; -} - -/** - * "Add filter" chip + popover pattern extracted for reuse. - * Accepts popover state from useAddFilterPopover hook. - */ -export const AddFilterButton: FC = ({ - idPrefix, - popoverState, - suggestions = [], -}) => { - const chipRef = useRef(null); - - const { - isOpen, - setIsOpen, - selectedColumnId, - columns, - filterType, - operator, - setOperator, - operatorOptions, - value, - setValue, - value2, - setValue2, - isValueDisabled, - isRangeOperator, - handleColumnChange, - commitAndClose, - cancelAndClose, - } = popoverState; - - return ( - <> - setIsOpen(true)} - /> - - - - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/BreadCrumbs.tsx b/src/inspect_scout/_view/www/src/app/components/BreadCrumbs.tsx deleted file mode 100644 index fd6189c77..000000000 --- a/src/inspect_scout/_view/www/src/app/components/BreadCrumbs.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import clsx from "clsx"; -import { FC, Fragment, useMemo, useRef } from "react"; -import { Link } from "react-router-dom"; - -import { basename, dirname } from "../../utils/path"; -import { prettyDirUri } from "../../utils/uri"; - -import styles from "./Breadcrumbs.module.css"; -import { - BreadcrumbSegment, - useBreadcrumbTruncation, -} from "./useBreadcrumbTruncation"; - -const kPathSeparator = "/"; - -interface BreadCrumbsProps { - // The base directory path (optional, e.g., "/home/user/logs") - // If provided, this will be displayed as the first two breadcrumb segments - baseDir?: string; - - // The current path to build breadcrumbs from (e.g., "foo/bar/baz") - relativePath: string; - - // Provides the route URL for a given segment path - getRouteForSegment?: (path: string) => string | undefined; - - // Whether to disable navigation for the last segment (defaults to false) - disableLastSegment?: boolean; - - className?: string | string[]; -} - -export const BreadCrumbs: FC = ({ - baseDir, - relativePath, - getRouteForSegment, - disableLastSegment = false, - className, -}) => { - const pathContainerRef = useRef(null); - - const breadcrumbSegments: BreadcrumbSegment[] = useMemo(() => { - const segments: BreadcrumbSegment[] = []; - - // Add base directory segments if provided - if (baseDir) { - // First segment - const parentDir = dirname(baseDir); - segments.push({ - text: prettyDirUri(parentDir), - url: undefined, - }); - // Second segment - segments.push({ - text: basename(baseDir), - url: getRouteForSegment ? getRouteForSegment("") : undefined, - }); - } - - // Build segments from the current path - const pathSegments = relativePath ? relativePath.split(kPathSeparator) : []; - const currentSegment: string[] = []; - - for (const pathSegment of pathSegments) { - currentSegment.push(pathSegment); - const fullSegmentPath = currentSegment.join(kPathSeparator); - segments.push({ - text: pathSegment, - url: getRouteForSegment - ? getRouteForSegment(fullSegmentPath) - : undefined, - }); - } - - return segments; - }, [baseDir, relativePath, getRouteForSegment]); - - const { visibleSegments, showEllipsis } = useBreadcrumbTruncation( - breadcrumbSegments, - pathContainerRef - ); - - return ( -
-
    - {visibleSegments.map((segment, index) => { - const isLast = index === visibleSegments.length - 1; - const shouldShowEllipsis = - showEllipsis && index === 1 && visibleSegments.length >= 2; - - return ( - - {shouldShowEllipsis && ( -
  1. - ... -
  2. - )} -
  3. - {segment.url && !(disableLastSegment && isLast) ? ( - {segment.text} - ) : ( - - {segment.text} - - )} -
  4. -
    - ); - })} -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/Breadcrumbs.module.css b/src/inspect_scout/_view/www/src/app/components/Breadcrumbs.module.css deleted file mode 100644 index 687b0ed2d..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Breadcrumbs.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.breadcrumbs { - --bs-breadcrumb-divider: "/"; - --bs-breadcrumb-item-padding-x: 0.1rem !important; - - margin-bottom: 0 !important; - min-width: 0; - overflow: hidden; - width: max-content; - display: flex; - flex-wrap: nowrap; -} - -.ellipsis { - color: var(--bs-secondary); - font-weight: normal; -} - -.pathContainer { - overflow-x: hidden; -} - -.pathLink { -} - -.pathSegment { -} diff --git a/src/inspect_scout/_view/www/src/app/components/Chip.module.css b/src/inspect_scout/_view/www/src/app/components/Chip.module.css deleted file mode 100644 index 823bc1bdb..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Chip.module.css +++ /dev/null @@ -1,29 +0,0 @@ -.chip { - display: inline-flex; - flex-direction: row; - margin: 0rem 0; - padding: 2px 6px; - border: solid 1px var(--bs-border-color); - border-radius: var(--bs-border-radius); - align-items: baseline; - white-space: nowrap; -} - -.label { - font-weight: 600; - margin-right: 0.5em; -} - -.icon { - margin-right: 0.2rem; - font-size: 0.9em; -} - -.closeIcon { - margin-left: 0.3rem; - font-size: 0.9em; -} - -.clickable { - cursor: pointer; -} diff --git a/src/inspect_scout/_view/www/src/app/components/Chip.tsx b/src/inspect_scout/_view/www/src/app/components/Chip.tsx deleted file mode 100644 index 4294aabe6..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Chip.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import clsx from "clsx"; -import { forwardRef } from "react"; - -import { ApplicationIcons } from "../../components/icons"; - -import styles from "./Chip.module.css"; - -interface ChipProps { - icon?: string; - label?: string; - value: string; - title?: string; - closeTitle?: string; - onClick?: () => void; - onClose?: (event?: React.MouseEvent | React.KeyboardEvent) => void; - className?: string | string[]; -} - -export const Chip = forwardRef( - ( - { icon, label, value, title, closeTitle, onClick, onClose, className }, - ref - ) => { - return ( -
- {icon ? ( - - ) : undefined} - {label ? ( - - {label} - - ) : undefined} - - {value} - - {onClose ? ( - { - event.stopPropagation(); - onClose(event); - }} - /> - ) : undefined} -
- ); - } -); - -Chip.displayName = "Chip"; diff --git a/src/inspect_scout/_view/www/src/app/components/ChipGroup.module.css b/src/inspect_scout/_view/www/src/app/components/ChipGroup.module.css deleted file mode 100644 index d0e0eb3fd..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ChipGroup.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.chipGroup { - display: flex; - flex-direction: row; - flex-wrap: wrap; - column-gap: 0.3rem; - row-gap: 0.3rem; -} diff --git a/src/inspect_scout/_view/www/src/app/components/ChipGroup.tsx b/src/inspect_scout/_view/www/src/app/components/ChipGroup.tsx deleted file mode 100644 index 116c9d462..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ChipGroup.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { clsx } from "clsx"; -import { FC } from "react"; - -import styles from "./ChipGroup.module.css"; - -interface ChipGroupProps { - className?: string | string[]; - children: React.ReactNode; -} - -export const ChipGroup: FC = ({ className, children }) => { - return
{children}
; -}; diff --git a/src/inspect_scout/_view/www/src/app/components/ColumnHeader.module.css b/src/inspect_scout/_view/www/src/app/components/ColumnHeader.module.css deleted file mode 100644 index 385e1c1a1..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ColumnHeader.module.css +++ /dev/null @@ -1,40 +0,0 @@ -.header { - display: flex; - align-items: center; - justify-content: space-between; - background-color: var(--bs-light-bg-subtle); - border-bottom: solid 1px var(--bs-border-color); - width: 100%; - padding-left: 0.5rem; -} - -.actions { - display: flex; - align-items: center; - gap: 4px; - margin-right: 0.25rem; -} - -.iconButton { - display: flex; - align-items: center; - justify-content: center; - width: 18px; - height: 18px; - font-size: 12px; - margin: 2px; - padding: 0; - background: transparent; - border: none; - border-radius: 4px; - color: var(--vscode-descriptionForeground); - cursor: pointer; - transition: - background-color 0.1s, - color 0.1s; -} - -.iconButton:hover { - background: var(--vscode-toolbar-hoverBackground); - color: var(--vscode-foreground); -} diff --git a/src/inspect_scout/_view/www/src/app/components/ColumnHeader.tsx b/src/inspect_scout/_view/www/src/app/components/ColumnHeader.tsx deleted file mode 100644 index 26b9f1757..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ColumnHeader.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import clsx from "clsx"; -import { ButtonHTMLAttributes, FC, forwardRef, ReactNode } from "react"; - -import styles from "./ColumnHeader.module.css"; - -interface ColumnHeaderProps { - label?: string; - actions?: ReactNode; -} - -export const ColumnHeader: FC = ({ label, actions }) => { - return ( -
-
- {label} -
- {actions &&
{actions}
} -
- ); -}; - -interface ColumnHeaderButtonProps extends ButtonHTMLAttributes { - icon: string; -} - -export const ColumnHeaderButton = forwardRef< - HTMLButtonElement, - ColumnHeaderButtonProps ->(({ icon, className, ...rest }, ref) => { - return ( - - ); -}); - -ColumnHeaderButton.displayName = "ColumnHeaderButton"; diff --git a/src/inspect_scout/_view/www/src/app/components/ColumnPickerButton.tsx b/src/inspect_scout/_view/www/src/app/components/ColumnPickerButton.tsx deleted file mode 100644 index 3b5b2ea87..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ColumnPickerButton.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { clsx } from "clsx"; -import { FC, ReactNode, useRef, useState } from "react"; - -import { ApplicationIcons } from "../../components/icons"; -import { ToolButton } from "../../components/ToolButton"; - -import styles from "./FilterBar.module.css"; - -export interface ColumnPickerRenderProps { - positionEl: HTMLButtonElement | null; - isOpen: boolean; - setIsOpen: (open: boolean) => void; -} - -export interface ColumnPickerButtonProps { - /** - * Render prop for the popover content. - * Receives positionEl, isOpen, and setIsOpen to wire up the popover. - */ - children: (props: ColumnPickerRenderProps) => ReactNode; -} - -/** - * Column picker button pattern extracted for reuse. - * Manages button ref and open/closed state internally. - * Uses render prop pattern to allow domain-specific popovers. - */ -export const ColumnPickerButton: FC = ({ - children, -}) => { - const buttonRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - - return ( - <> - setIsOpen(!isOpen)} - subtle - className={clsx("text-size-smallest", styles.columnsButton)} - /> - {children({ - // eslint-disable-next-line react-hooks/refs -- positionEl accepts null; PopOver/Popper handles this in effects and updates when ref is populated - positionEl: buttonRef.current, - isOpen, - setIsOpen, - })} - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/ColumnsPopover.module.css b/src/inspect_scout/_view/www/src/app/components/ColumnsPopover.module.css deleted file mode 100644 index 35c22c46c..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ColumnsPopover.module.css +++ /dev/null @@ -1,43 +0,0 @@ -.columnList { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.15em 1em; - max-height: 400px; - overflow-y: auto; -} - -.row { - display: flex; - align-items: center; - gap: 0.5em; - cursor: pointer; - border-radius: var(--bs-border-radius); - padding: 0.1em 0.4em; - margin: 0 -0.4em; -} - -.row:hover { - background-color: var(--bs-secondary-bg); -} - -.links { - display: flex; - padding-bottom: 0.2em; - margin-bottom: 0.4em; - column-gap: 0.3em; - border-bottom: solid 1px var(--bs-border-color); -} - -.links a { - cursor: pointer; - text-decoration: none; - color: var(--bs-link-color); -} - -.links a:hover { - color: var(--bs-link-hover-color); -} - -.selected { - font-weight: 600; -} diff --git a/src/inspect_scout/_view/www/src/app/components/ColumnsPopover.tsx b/src/inspect_scout/_view/www/src/app/components/ColumnsPopover.tsx deleted file mode 100644 index 71fa0065b..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ColumnsPopover.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import clsx from "clsx"; -import { FC, useCallback, useMemo } from "react"; - -import { PopOver } from "../../components/PopOver"; - -import styles from "./ColumnsPopover.module.css"; - -export interface ColumnInfo { - id: string; - label: string; - headerTitle?: string; -} - -export interface ColumnsPopoverProps { - /** Popover positioning element */ - positionEl: HTMLElement | null; - /** Whether the popover is open */ - isOpen: boolean; - /** Callback to set open state */ - setIsOpen: (open: boolean) => void; - /** All available columns in display order */ - columns: ColumnInfo[]; - /** Currently visible column IDs */ - visibleColumns: string[]; - /** Default visible column IDs (for "Default" link) */ - defaultVisibleColumns: string[]; - /** Callback when visible columns change */ - onVisibleColumnsChange: (columns: string[]) => void; - /** Optional popover ID for accessibility */ - popoverId?: string; -} - -export const ColumnsPopover: FC = ({ - positionEl, - isOpen, - setIsOpen, - columns, - visibleColumns, - defaultVisibleColumns, - onVisibleColumnsChange, - popoverId = "columns-popover", -}) => { - const isDefaultSelection = useMemo( - () => - visibleColumns.length === defaultVisibleColumns.length && - visibleColumns.every((col) => defaultVisibleColumns.includes(col)), - [visibleColumns, defaultVisibleColumns] - ); - - const isAllSelection = useMemo( - () => visibleColumns.length === columns.length, - [visibleColumns, columns] - ); - - const setDefaultSelection = useCallback(() => { - onVisibleColumnsChange(defaultVisibleColumns); - }, [onVisibleColumnsChange, defaultVisibleColumns]); - - const setAllSelection = useCallback(() => { - onVisibleColumnsChange(columns.map((c) => c.id)); - }, [onVisibleColumnsChange, columns]); - - const toggleColumn = useCallback( - (columnId: string, show: boolean) => { - if (show && !visibleColumns.includes(columnId)) { - onVisibleColumnsChange([...visibleColumns, columnId]); - } else if (!show) { - onVisibleColumnsChange(visibleColumns.filter((c) => c !== columnId)); - } - }, - [visibleColumns, onVisibleColumnsChange] - ); - - return ( - - - -
- {columns.map((column) => ( -
{ - toggleColumn(column.id, !visibleColumns.includes(column.id)); - }} - > - { - toggleColumn(column.id, e.target.checked); - }} - /> - {column.label} -
- ))} -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/EditablePath.tsx b/src/inspect_scout/_view/www/src/app/components/EditablePath.tsx deleted file mode 100644 index 90fc40d32..000000000 --- a/src/inspect_scout/_view/www/src/app/components/EditablePath.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { FC } from "react"; - -import { isUri, prettyDirUri } from "../../utils/uri"; - -import { EditableText } from "./EditableText"; - -interface EditablePathProps { - path?: string | null; - secondaryText?: string | null; - onPathChanged: (path: string) => void; - - mru?: string[]; - - label?: string; - title?: string; - - icon?: string; - placeholder?: string; - - editable?: boolean; - - className?: string; -} - -export const EditablePath: FC = ({ - path, - secondaryText, - onPathChanged, - mru, - label, - title, - icon, - placeholder, - editable = true, - className, -}) => { - // Format a local path without the file:// prefix - const displayPath = prettyDirUri(path || ""); - - const onValueChanged = (newDisplayPath: string) => { - if (isUri(newDisplayPath)) { - onPathChanged(newDisplayPath); - } else { - const newUri = `file://${newDisplayPath}`; - onPathChanged(newUri); - } - }; - - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/EditableText.module.css b/src/inspect_scout/_view/www/src/app/components/EditableText.module.css deleted file mode 100644 index 3f8b97cd0..000000000 --- a/src/inspect_scout/_view/www/src/app/components/EditableText.module.css +++ /dev/null @@ -1,106 +0,0 @@ -.container { - display: grid; - grid-template-columns: max-content max-content minmax(0, max-content); - align-items: center; - border: solid 1px var(--bs-border-color); - border-radius: var(--bs-border-radius); - width: 100%; -} - -.labelContainer { - border-right: solid 1px var(--bs-border-color); - padding: 0 0.45rem; - height: 100%; - display: flex; - align-items: center; -} - -.labelAdornmentIcon { - margin-left: 0.5rem; -} - -.icon { - color: var(--bs-body-color); - opacity: 0.7; - margin-right: 0.35rem; -} - -.text { - color: var(--bs-body-color); - white-space: nowrap; - cursor: text; - padding: 0.125rem 0.25rem; - border-top-right-radius: var(--bs-border-radius); - border-bottom-right-radius: var(--bs-border-radius); - transition: background-color 0.2s; - min-width: 2ch; - display: inline-block; - background-color: var(--bs-body-bg); -} - -.text.readOnly { - background-color: var(--bs-light); -} - -.text:hover { - background-color: var(--bs-secondary-bg); -} - -.text:focus { - outline: 1px solid var(--bs-primary); - outline-offset: 0; - background-color: var(--bs-body-bg); -} - -.text.placeholder { - color: var(--bs-secondary-color); - opacity: 0.6; -} - -.text.secondary { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.mruPopover { - padding: 0 !important; - max-height: 300px; - overflow-y: auto; - width: 80%; -} - -.mruList { - display: flex; - flex-direction: column; - min-width: 150px; -} - -.mruItem { - padding: 0.3rem 0.5rem; - cursor: pointer; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - transition: background-color 0.15s; - color: var(--bs-body-color); -} - -.mruItem:hover, -.mruItemSelected { - background-color: var(--bs-secondary-bg); -} - -.mruItem:active { - background-color: var(--bs-tertiary-bg); -} - -.mruItem:first-child { - border-top-left-radius: var(--bs-border-radius); - border-top-right-radius: var(--bs-border-radius); -} - -.mruItem:last-child { - border-bottom-left-radius: var(--bs-border-radius); - border-bottom-right-radius: var(--bs-border-radius); -} diff --git a/src/inspect_scout/_view/www/src/app/components/EditableText.tsx b/src/inspect_scout/_view/www/src/app/components/EditableText.tsx deleted file mode 100644 index 24fcaf90b..000000000 --- a/src/inspect_scout/_view/www/src/app/components/EditableText.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import clsx from "clsx"; -import { FC, useRef, useCallback, useState, useEffect, useMemo } from "react"; - -import { PopOver } from "../../components/PopOver"; - -import styles from "./EditableText.module.css"; - -interface EditableTextProps { - value?: string; - secondaryValue?: string | null; - onValueChanged: (value: string) => void; - - mru?: string[]; - mruMaxItems?: number; - - label?: string; - title?: string; - - icon?: string; - placeholder?: string; - - editable?: boolean; - - className?: string; -} - -export const EditableText: FC = ({ - value, - secondaryValue, - onValueChanged, - mru, - mruMaxItems = 10, - icon, - label, - title, - placeholder, - editable = true, - className, -}) => { - const spanRef = useRef(null); - const initialValueRef = useRef(""); - const containerRef = useRef(null); - - // MRU popover state: This isn't in the store because the values are transient (if the state is - // restored, it is assumed that the popover should be closed / state discarded) - const [showMruPopover, setShowMruPopover] = useState(false); - const [selectedMruIndex, setSelectedMruIndex] = useState(-1); - const [currentText, setCurrentText] = useState(""); - const [isEditing, setIsEditing] = useState(false); - const [isFocused, setIsFocused] = useState(false); - - // Filter MRU list based on current text - const filteredMru = useMemo(() => { - if (!mru || mru.length === 0) return []; - - // If not editing yet (just focused) or current text equals the initial value, - // show first mruMaxItems items - // eslint-disable-next-line react-hooks/refs -- initialValueRef stores a snapshot set in handleFocus; safe to read as it doesn't need to trigger re-renders - if (!isEditing || currentText === initialValueRef.current) { - return mru.slice(0, mruMaxItems); - } - - // Otherwise, filter and limit to mruMaxItems - const filtered = mru - .filter((item) => - item.toLowerCase().startsWith(currentText.toLowerCase()) - ) - .slice(0, mruMaxItems); - - // If no matches, still show all items so the popover doesn't disappear - return filtered.length > 0 ? filtered : mru.slice(0, mruMaxItems); - }, [mru, mruMaxItems, currentText, isEditing]); - - // Update showMruPopover based on whether we have filtered items and focus state - useEffect(() => { - setShowMruPopover(filteredMru.length > 0 && isFocused); - }, [filteredMru, isFocused]); - - const handleFocus = () => { - // Store the initial value when focusing - if (spanRef.current) { - initialValueRef.current = spanRef.current.textContent || ""; - setCurrentText(initialValueRef.current); - setSelectedMruIndex(-1); - setIsEditing(false); // Reset editing state on focus - setIsFocused(true); // Mark as focused - - // Select all text on focus - const range = document.createRange(); - range.selectNodeContents(spanRef.current); - const selection = window.getSelection(); - selection?.removeAllRanges(); - selection?.addRange(range); - } - }; - - const commitChanges = useCallback(() => { - if (spanRef.current) { - const newValue = spanRef.current.textContent?.trim() || ""; - if (newValue !== "" && newValue !== initialValueRef.current) { - onValueChanged(newValue); - } else if (newValue === "") { - // Restore the original value if empty - spanRef.current.textContent = initialValueRef.current; - } - } - setShowMruPopover(false); - setSelectedMruIndex(-1); - setIsEditing(false); - }, [onValueChanged]); - - const selectMruItem = useCallback( - (item: string | undefined) => { - if (spanRef.current && item) { - spanRef.current.textContent = item; - setCurrentText(item); - setShowMruPopover(false); - setSelectedMruIndex(-1); - setIsEditing(false); - spanRef.current.blur(); - if (item !== initialValueRef.current) { - onValueChanged(item); - } - } - }, - [onValueChanged] - ); - - const handleBlur = () => { - // Delay both focus state and commit changes to allow click on MRU item to register - setTimeout(() => { - setIsFocused(false); // Mark as not focused - commitChanges(); - }, 150); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - // Handle MRU navigation when popover is open - if (showMruPopover && filteredMru.length > 0) { - if (e.key === "ArrowDown") { - e.preventDefault(); - setSelectedMruIndex((prev) => - prev < filteredMru.length - 1 ? prev + 1 : prev - ); - return; - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setSelectedMruIndex((prev) => (prev > 0 ? prev - 1 : -1)); - return; - } else if (e.key === "Enter") { - e.preventDefault(); - if (selectedMruIndex >= 0 && selectedMruIndex < filteredMru.length) { - selectMruItem(filteredMru[selectedMruIndex]); - } else { - spanRef.current?.blur(); - } - return; - } else if (e.key === "Escape") { - e.preventDefault(); - setShowMruPopover(false); - setSelectedMruIndex(-1); - // Restore original value on escape - if (spanRef.current) { - spanRef.current.textContent = initialValueRef.current; - setCurrentText(initialValueRef.current); - } - spanRef.current?.blur(); - return; - } - } else { - // Default behavior when popover is not open - if (e.key === "Enter") { - e.preventDefault(); - spanRef.current?.blur(); - } else if (e.key === "Escape") { - e.preventDefault(); - // Restore original value on escape - if (spanRef.current) { - spanRef.current.textContent = initialValueRef.current; - setCurrentText(initialValueRef.current); - } - spanRef.current?.blur(); - } - } - }; - - const handleInput = () => { - // Update current text for filtering - if (spanRef.current) { - const text = spanRef.current.textContent || ""; - setCurrentText(text); - setSelectedMruIndex(-1); // Reset selection when user types - setIsEditing(true); // Mark as editing when user types - } - - // Prevent empty content from collapsing the span - if (spanRef.current && spanRef.current.textContent === "") { - spanRef.current.textContent = ""; - } - }; - - const displayValue = value || placeholder || ""; - - return ( - <> -
-
- {icon && } - {label && {label}} -
- - {displayValue} - - {secondaryValue && ( - - {secondaryValue} - - )} -
- - {mru && mru.length > 0 && ( - -
- {filteredMru.map((item, index) => ( -
selectMruItem(item)} - onMouseEnter={() => setSelectedMruIndex(index)} - > - {item} -
- ))} -
-
- )} - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/Error.module.css b/src/inspect_scout/_view/www/src/app/components/Error.module.css deleted file mode 100644 index 760031a4c..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Error.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.error i:global(.bi:before) { - color: var(--bs-danger) !important; - margin-right: 0.3rem; -} - -.refusal i:global(.bi:before) { - color: var(--bs-primary) !important; - margin-right: 0.3rem; -} diff --git a/src/inspect_scout/_view/www/src/app/components/Error.tsx b/src/inspect_scout/_view/www/src/app/components/Error.tsx deleted file mode 100644 index 3526db8a2..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Error.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { ApplicationIcons } from "../../components/icons"; - -import styles from "./Error.module.css"; - -interface ErrorProps { - error: string; - refusal?: boolean; -} - -export const Error: FC = ({ error, refusal }) => { - const icon = refusal ? ApplicationIcons.refuse : ApplicationIcons.error; - const message = refusal ? "Refusal" : error; - return ( -
- - {message} -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/Explanation.tsx b/src/inspect_scout/_view/www/src/app/components/Explanation.tsx deleted file mode 100644 index f43cadd1b..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Explanation.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { FC, ReactNode } from "react"; - -import { - MarkdownDivWithReferences, - MarkdownReference, -} from "../../components/MarkdownDivWithReferences"; -import { ScanResultSummary } from "../types"; - -interface ExplanationProps { - summary?: ScanResultSummary; - references?: MarkdownReference[]; - options?: { - previewRefsOnHover?: boolean; - }; -} - -export const Explanation: FC = ({ - summary, - references, - options, -}): ReactNode => { - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/FilterBar.module.css b/src/inspect_scout/_view/www/src/app/components/FilterBar.module.css deleted file mode 100644 index 839f0d929..000000000 --- a/src/inspect_scout/_view/www/src/app/components/FilterBar.module.css +++ /dev/null @@ -1,66 +0,0 @@ -.container { - display: flex; - flex-direction: row; - background: var(--bs-light-bg-subtle); - border-bottom: solid var(--bs-border-color) 1px; - padding: 0rem 0.5rem; - align-items: flex-start; - width: 100%; -} - -.filterBar { - margin: 1px 0; -} - -.filterLabel { - margin-right: 0.3rem; - margin-top: 3px; -} - -.filterNone { - margin-top: 2px; -} - -.actionButtons { - margin-left: auto; - margin-top: 0.1rem; - margin-bottom: 0.1rem; - display: flex; - justify-content: flex-end; - align-items: flex-start; - flex-direction: row; - font-size: var(--inspect-font-size-smallest); - height: calc(100% - 4px); -} - -.actionButton { - padding: 0.1rem 0.4rem; - margin: 0; - font-size: var(--inspect-font-size-smallest); -} - -.actionButtonDropDown { - font-size: var(--inspect-font-size-smallest); -} - -.actionButton.chipButton { - background-color: unset; - margin-left: 0.3rem; -} - -.sep { - width: 1px; - height: calc(100% - 2px); - margin: 2px 0.3rem; - - background-color: var(--bs-border-color); - font-size: var(--inspect-font-size-smallestest); -} - -.filterChip { - /* Filter chip specific styles can go here */ -} - -.columnsButton { - padding: 0.1rem 0.3rem; -} diff --git a/src/inspect_scout/_view/www/src/app/components/FilterBar.tsx b/src/inspect_scout/_view/www/src/app/components/FilterBar.tsx deleted file mode 100644 index 4eca2bf89..000000000 --- a/src/inspect_scout/_view/www/src/app/components/FilterBar.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import { clsx } from "clsx"; -import { FC, useCallback, useRef, useState } from "react"; - -import { ScalarValue } from "../../api/api"; -import { ApplicationIcons } from "../../components/icons"; -import { PopOver } from "../../components/PopOver"; -import { ToolDropdownButton } from "../../components/ToolDropdownButton"; -import type { SimpleCondition } from "../../query/types"; -import type { ColumnFilter } from "../../state/store"; - -import { AddFilterButton, type AddFilterPopoverState } from "./AddFilterButton"; -import { Chip } from "./Chip"; -import { ChipGroup } from "./ChipGroup"; -import { - ColumnFilterEditor, - useColumnFilterPopover, - type AvailableColumn, -} from "./columnFilter"; -import { ColumnPickerButton } from "./ColumnPickerButton"; -import { ColumnsPopover, type ColumnInfo } from "./ColumnsPopover"; -import styles from "./FilterBar.module.css"; - -const kCopyCodeDescriptors = [ - { label: "Code (Python)", value: "python" }, - { label: "Filter (SQL)", value: "filter" }, -]; - -export interface FilterBarProps { - /** Current column filters */ - filters: Record; - /** Callback when a filter condition is changed */ - onFilterChange: (columnId: string, condition: SimpleCondition | null) => void; - /** Callback when a filter is removed */ - onRemoveFilter: (columnId: string) => void; - /** Optional code representations for copy functionality */ - filterCodeValues?: Record; - /** Autocomplete suggestions for filter values */ - filterSuggestions?: ScalarValue[]; - /** Callback when filter column selection changes (for fetching suggestions) */ - onFilterColumnChange?: (columnId: string | null) => void; - /** Unique ID prefix for popovers */ - popoverIdPrefix?: string; - - // Add filter button config - /** Popover state from useAddFilterPopover hook */ - addFilterPopoverState: AddFilterPopoverState; - - // Column picker config (optional - omit to hide) - /** Column definitions for the picker */ - columns?: ColumnInfo[]; - /** Currently visible column IDs */ - visibleColumns?: string[]; - /** Default visible column IDs */ - defaultVisibleColumns?: string[]; - /** Callback when visible columns change */ - onVisibleColumnsChange?: (columns: string[]) => void; -} - -export const FilterBar: FC = ({ - filters, - onFilterChange, - onRemoveFilter, - filterCodeValues, - filterSuggestions = [], - onFilterColumnChange, - popoverIdPrefix = "filter", - addFilterPopoverState, - columns, - visibleColumns, - defaultVisibleColumns, - onVisibleColumnsChange, -}) => { - // Filter editing state - const [editingColumnId, setEditingColumnId] = useState(null); - const chipRefs = useRef>({}); - - const editingFilter = editingColumnId ? filters[editingColumnId] : null; - - const handleFilterChange = useCallback( - (condition: SimpleCondition | null) => { - if (!editingColumnId) return; - onFilterChange(editingColumnId, condition); - }, - [editingColumnId, onFilterChange] - ); - - const { - isOpen: isEditorOpen, - setIsOpen: setIsEditorOpen, - operator, - setOperator, - operatorOptions, - value: rawValue, - setValue: setRawValue, - value2: rawValue2, - setValue2: setRawValue2, - isValueDisabled, - isRangeOperator, - commitAndClose, - cancelAndClose, - } = useColumnFilterPopover({ - columnId: editingColumnId ?? "", - filterType: editingFilter?.filterType ?? "string", - condition: editingFilter?.condition ?? null, - onChange: handleFilterChange, - }); - - const editFilter = useCallback( - (columnId: string) => () => { - setEditingColumnId(columnId); - setIsEditorOpen(true); - onFilterColumnChange?.(columnId); - }, - [setIsEditorOpen, onFilterColumnChange] - ); - - // Notify parent when editor closes - const handleEditorOpenChange = useCallback( - (open: boolean) => { - setIsEditorOpen(open); - if (!open) { - onFilterColumnChange?.(null); - } - }, - [setIsEditorOpen, onFilterColumnChange] - ); - - const filterEntries = Object.values(filters); - - return ( -
-
- Filter: -
- - {filterEntries.length > 0 - ? filterEntries.map((filter) => { - if (!filter || !filter.condition) { - return null; - } - return ( - { - chipRefs.current[filter.columnId] = el; - }} - label={filter.columnId} - value={formatFilterCondition(filter.condition)} - title={`Edit ${filter.columnId} filter`} - closeTitle="Remove filter" - className={clsx(styles.filterChip, "text-size-smallest")} - onClose={() => { - onRemoveFilter(filter.columnId); - }} - onClick={editFilter(filter.columnId)} - /> - ); - }) - : null} - {/* Add Filter Button - rendered internally */} - - - - {/* Edit filter popover */} - {editingColumnId && editingFilter && ( - - - - )} - -
- {filterCodeValues !== undefined && ( - - )} - {/* Column Picker - rendered if columns and handler provided */} - {columns && onVisibleColumnsChange && ( - <> -
- - {({ positionEl, isOpen, setIsOpen }) => ( - - )} - - - )} -
-
- ); -}; - -const CopyQueryButton: FC<{ itemValues?: Record }> = ({ - itemValues, -}) => { - const [icon, setIcon] = useState(ApplicationIcons.copy); - - const items = kCopyCodeDescriptors.reduce( - (acc, desc) => { - acc[desc.label] = () => { - const text = itemValues ? itemValues[desc.value] : ""; - if (!text) { - return; - } - - void navigator.clipboard.writeText(text); - setIcon(ApplicationIcons.confirm); - setTimeout(() => { - setIcon(ApplicationIcons.copy); - }, 1250); - }; - return acc; - }, - {} as Record void> - ); - - return ( - - ); -}; - -const formatRepresentativeType = (value: unknown): string => { - if (value === null) { - return "NULL"; - } else if (Array.isArray(value)) { - return `[${value.map((v) => formatRepresentativeType(v)).join(", ")}]`; - } else if (typeof value === "object") { - return "{...}"; - } else if (typeof value === "string") { - return `'${value}'`; - } else { - return String(value); - } -}; - -/** - * Formats a filter condition for display in a chip. - * Handles special formatting for BETWEEN, IN, and other operators. - */ -const formatFilterCondition = (condition: SimpleCondition): string => { - const { operator, right } = condition; - - // BETWEEN / NOT BETWEEN: show as "BETWEEN value1 AND value2" - if ( - (operator === "BETWEEN" || operator === "NOT BETWEEN") && - Array.isArray(right) && - right.length === 2 - ) { - const v1 = formatRepresentativeType(right[0]); - const v2 = formatRepresentativeType(right[1]); - return `${operator} ${v1} AND ${v2}`; - } - - // IN / NOT IN: show as "IN (value1, value2, ...)" - if ((operator === "IN" || operator === "NOT IN") && Array.isArray(right)) { - const values = right.map((v) => formatRepresentativeType(v)).join(", "); - return `${operator} (${values})`; - } - - // IS NULL / IS NOT NULL: no value needed - if (operator === "IS NULL" || operator === "IS NOT NULL") { - return operator; - } - - // Default: "operator value" - return `${operator} ${formatRepresentativeType(right)}`; -}; - -// Export types for convenience -export type { AddFilterPopoverState, AvailableColumn, ColumnInfo }; diff --git a/src/inspect_scout/_view/www/src/app/components/Footer.module.css b/src/inspect_scout/_view/www/src/app/components/Footer.module.css deleted file mode 100644 index 28fcc8a1f..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Footer.module.css +++ /dev/null @@ -1,55 +0,0 @@ -.footer { - border-top: solid var(--bs-light-border-subtle) 1px; - background: var(--bs-light-bg-subtle); - display: grid; - grid-template-columns: max-content 1fr max-content; - justify-content: space-between; - - padding: 0.2em 1em; -} - -.spinnerContainer { - display: grid; - grid-template-columns: max-content max-content; - column-gap: 0.3em; - padding-top: 0.2em; -} - -.spinner { - height: 11px !important; - width: 11px !important; - color: var(--bs-secondary) !important; - border-width: 1px !important; -} - -.label { - margin-left: 0.1em; - margin-top: -3px; -} - -.right { - display: grid; - grid-auto-columns: max-content; - grid-auto-flow: column; - justify-content: end; - align-items: center; - column-gap: 0.5em; -} - -.left { - display: grid; - grid-auto-columns: max-content; - grid-auto-flow: column; - justify-content: start; - align-items: center; - column-gap: 0.5em; -} - -.center { - display: grid; - grid-auto-columns: max-content; - grid-auto-flow: column; - justify-content: center; - align-items: center; - column-gap: 0.5em; -} diff --git a/src/inspect_scout/_view/www/src/app/components/Footer.tsx b/src/inspect_scout/_view/www/src/app/components/Footer.tsx deleted file mode 100644 index 73125ab8d..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Footer.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import clsx from "clsx"; -import { FC, ReactNode } from "react"; - -import { formatNumber } from "../../utils/format"; - -import styles from "./Footer.module.css"; -import { Pager } from "./Pager"; - -interface FooterProps { - id: string; - className?: string | string[]; - - // Items - itemCount?: number; - filteredCount?: number; - - // left side controls (replaced by progress when there is progress) - left?: ReactNode; - - // Progress - progressText?: string; - progressBar?: ReactNode; - - // Pagination - paginated: boolean; - pagesize?: number; - page?: number; - itemsPerPage?: number; - setPage?: (page: number) => void; - - // labels - labels?: { - singular: string; - plural: string; - }; -} - -export const Footer: FC = ({ - id, - className, - itemCount = 0, - paginated, - filteredCount, - left, - progressText, - progressBar, - page, - setPage, - itemsPerPage = -1, - labels = { - singular: "item", - plural: "items", - }, -}) => { - // Get filtered count from the store - const effectiveItemCount = filteredCount ?? itemCount; - - // Compute the start and end items - const currentPage = page || 0; - const pageItemCount = Math.min( - itemsPerPage, - effectiveItemCount - currentPage * itemsPerPage - ); - const startItem = effectiveItemCount > 0 ? currentPage * itemsPerPage + 1 : 0; - const endItem = startItem + pageItemCount - 1; - - return ( -
-
- {progressText ? ( -
-
- {progressText}... -
-
- {progressText}... -
-
- ) : ( - left - )} -
-
- {paginated && ( - - )} -
-
- {progressBar ? ( - progressBar - ) : paginated ? ( -
- {effectiveItemCount === 0 - ? "" - : filteredCount !== undefined && filteredCount !== itemCount - ? `${startItem} - ${endItem} / ${effectiveItemCount} (${itemCount} total)` - : `${startItem} - ${endItem} / ${effectiveItemCount}`} -
- ) : effectiveItemCount === 1 ? ( - `${formatNumber(effectiveItemCount)} ${labels.singular}` - ) : ( - `${formatNumber(effectiveItemCount)} ${labels.plural}` - )} -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/HeadingGrid.module.css b/src/inspect_scout/_view/www/src/app/components/HeadingGrid.module.css deleted file mode 100644 index 4da4ce3d3..000000000 --- a/src/inspect_scout/_view/www/src/app/components/HeadingGrid.module.css +++ /dev/null @@ -1,39 +0,0 @@ -.headingGrid { - display: grid; - gap: 1rem; -} - -.headingCell { - display: flex; - align-items: flex-start; - min-width: 0; /* Allow flex items to shrink below their content size */ -} - -/* Horizontal layouts (left/right) */ -.headingCell.horizontal { - flex-direction: row; - gap: 0.5rem; -} - -/* Vertical layouts (above/below) */ -.headingCell.vertical { - flex-direction: column; - align-items: flex-start; - gap: 0rem; -} - -.label { - font-weight: 500; - white-space: nowrap; - flex-shrink: 0; /* Prevent labels from shrinking */ -} - -.value { - min-width: 0; /* Allow values to shrink */ - word-break: break-word; /* Allow long words to break */ -} - -/* For vertical layouts, allow value to take full width */ -.vertical .value { - width: 100%; -} diff --git a/src/inspect_scout/_view/www/src/app/components/HeadingGrid.tsx b/src/inspect_scout/_view/www/src/app/components/HeadingGrid.tsx deleted file mode 100644 index 3440517a1..000000000 --- a/src/inspect_scout/_view/www/src/app/components/HeadingGrid.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import clsx from "clsx"; -import { FC, ReactNode } from "react"; - -import styles from "./HeadingGrid.module.css"; - -export interface HeadingValue { - label: string; - value: ReactNode; - labelPosition?: "left" | "right" | "above" | "below"; -} - -interface HeadingGridProps { - headings: HeadingValue[]; - rows?: number; - columns?: number; - className?: string | string[]; - labelClassName?: string | string[]; - valueClassName?: string | string[]; -} - -export const HeadingGrid: FC = ({ - headings, - rows, - columns, - className, - labelClassName, - valueClassName, -}) => { - // Default to 1 row and N columns (where N is the number of headings) - const actualColumns = columns ?? headings.length; - const actualRows = rows ?? 1; - - // Calculate the grid size - const totalCells = actualRows * actualColumns; - - // Pad the headings array if needed or slice if too many - const gridItems = headings.slice(0, totalCells); - - return ( -
- {gridItems.map((heading, index) => ( - - ))} -
- ); -}; - -interface HeadingCellProps { - heading: HeadingValue; - labelClassName?: string | string[]; - valueClassName?: string | string[]; -} - -const HeadingCell: FC = ({ - heading, - labelClassName, - valueClassName, -}) => { - const { label, value, labelPosition = "above" } = heading; - - // Render based on label position - switch (labelPosition) { - case "left": - return ( -
- {label} - {value} -
- ); - case "right": - return ( -
- {value} - {label} -
- ); - case "above": - return ( -
- {label} - {value} -
- ); - case "below": - return ( -
- {value} - {label} -
- ); - } -}; diff --git a/src/inspect_scout/_view/www/src/app/components/Identifier.module.css b/src/inspect_scout/_view/www/src/app/components/Identifier.module.css deleted file mode 100644 index 8a710ad2d..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Identifier.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.id { - display: grid; - grid-template-rows: auto auto; - row-gap: 0; -} - -.idContainer { - overflow-wrap: anywhere; -} diff --git a/src/inspect_scout/_view/www/src/app/components/Identifier.tsx b/src/inspect_scout/_view/www/src/app/components/Identifier.tsx deleted file mode 100644 index 9e3275432..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Identifier.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import clsx from "clsx"; -import { FC, ReactNode } from "react"; - -import { ScanResultSummary } from "../types"; -import { resultIdentifier } from "../utils/results"; - -import styles from "./Identifier.module.css"; - -interface IndentifierProps { - summary: ScanResultSummary; -} - -export const Identifier: FC = ({ summary }): ReactNode => { - const identifier = resultIdentifier(summary); - if (identifier.epoch) { - const id = identifier.id; - const epoch = identifier.epoch; - return ( -
-
{id}
-
- {identifier.secondaryId ? `${identifier.secondaryId} ` : ""}epoch{" "} - {epoch} -
-
- ); - } else { - return identifier.id; - } -}; diff --git a/src/inspect_scout/_view/www/src/app/components/NavButtons.module.css b/src/inspect_scout/_view/www/src/app/components/NavButtons.module.css deleted file mode 100644 index 5daccbd63..000000000 --- a/src/inspect_scout/_view/www/src/app/components/NavButtons.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.toolbarButton { - color: var(--bs-body-color); -} - -.toolbarButton:hover { - color: var(--bs-link-hover-color); -} - -.toolbarButton.disabled { - color: var(--bs-secondary); -} - -.toolbarButton.disabled:hover { - color: var(--bs-secondary); - cursor: default; -} diff --git a/src/inspect_scout/_view/www/src/app/components/NavButtons.tsx b/src/inspect_scout/_view/www/src/app/components/NavButtons.tsx deleted file mode 100644 index 90d75da62..000000000 --- a/src/inspect_scout/_view/www/src/app/components/NavButtons.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; -import { Link } from "react-router-dom"; - -import styles from "./NavButtons.module.css"; - -export interface NavButton { - // The title of the navigation button - title: string; - - // The icon class for the navigation button - icon: string; - - // The route URL for the navigation button - route: string; - - // Whether the button is enabled or disabled (optional) - enabled?: boolean; -} - -interface NavButtonsProps { - buttons: NavButton[]; -} - -export const NavButtons: FC = ({ buttons }) => { - return ( - <> - {buttons.map((button, index) => - button.enabled !== false ? ( - - - - ) : ( - - - - ) - )} - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/Navbar.module.css b/src/inspect_scout/_view/www/src/app/components/Navbar.module.css deleted file mode 100644 index 2041e99b1..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Navbar.module.css +++ /dev/null @@ -1,42 +0,0 @@ -.header { - background: var(--bs-light); - display: grid; - grid-template-columns: max-content 1fr max-content max-content; - justify-content: space-between; - padding: 0.2em 0.5em; - overflow: hidden; -} - -.bordered { - border-bottom: solid var(--bs-border-color) 1px; -} - -.left { - overflow: hidden; - min-width: 0; - max-width: 100%; -} - -.leftButtons { - display: flex; - gap: 0.4rem; - align-items: baseline; - padding-right: 0.4rem; - margin-right: 0.4rem; - border-right: solid var(--bs-border-color) 1px; -} - -.rightButtons { - display: flex; - gap: 0.4rem; - align-items: center; - padding-left: 0.4rem; - margin-left: 0.4rem; - border-left: solid var(--bs-border-color) 1px; -} - -.hasChildren { - border-left: solid var(--bs-border-color) 1px; - padding-left: 0.5rem; - margin-left: 0.5rem; -} diff --git a/src/inspect_scout/_view/www/src/app/components/Navbar.tsx b/src/inspect_scout/_view/www/src/app/components/Navbar.tsx deleted file mode 100644 index ab444eca4..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Navbar.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import clsx from "clsx"; -import { FC, ReactNode } from "react"; - -import styles from "./Navbar.module.css"; -import { NavButton, NavButtons } from "./NavButtons"; - -interface NavbarProps { - leftButtons?: NavButton[]; - left?: ReactNode; - right?: ReactNode; - rightButtons?: NavButton[]; - bordered?: boolean; -} - -export const Navbar: FC = ({ - bordered = true, - left, - right, - leftButtons, - rightButtons, -}) => { - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/NextPreviousNav.module.css b/src/inspect_scout/_view/www/src/app/components/NextPreviousNav.module.css deleted file mode 100644 index ece5d21dd..000000000 --- a/src/inspect_scout/_view/www/src/app/components/NextPreviousNav.module.css +++ /dev/null @@ -1,20 +0,0 @@ -.container { - display: grid; - grid-template-columns: max-content auto max-content; - align-items: center; - gap: 0.5rem; - margin-right: 1em; -} - -.nav:hover:not(.disabled) { - cursor: pointer; -} - -.disabled { - opacity: 0.5; - pointer-events: none; -} - -.center { - text-align: center; -} diff --git a/src/inspect_scout/_view/www/src/app/components/NextPreviousNav.tsx b/src/inspect_scout/_view/www/src/app/components/NextPreviousNav.tsx deleted file mode 100644 index 3db8cd0b9..000000000 --- a/src/inspect_scout/_view/www/src/app/components/NextPreviousNav.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import clsx from "clsx"; -import { FC, ReactNode, useEffect } from "react"; - -import { ApplicationIcons } from "../../components/icons"; - -import styles from "./NextPreviousNav.module.css"; - -interface NextPreviousNavProps { - onPrevious?: () => void; - onNext?: () => void; - hasPrevious: boolean; - hasNext: boolean; - children?: ReactNode; - enableKeyboardNav?: boolean; -} - -export const NextPreviousNav: FC = ({ - onPrevious, - onNext, - hasPrevious, - hasNext, - children, - enableKeyboardNav = true, -}) => { - // Global keydown handler for keyboard shortcuts - useEffect(() => { - if (!enableKeyboardNav) { - return; - } - - const handleGlobalKeyDown = (e: globalThis.KeyboardEvent) => { - // Don't handle keyboard events if focus is on an input, textarea, or select element - const activeElement = document.activeElement; - const isInputFocused = - activeElement && - (activeElement.tagName === "INPUT" || - activeElement.tagName === "TEXTAREA" || - activeElement.tagName === "SELECT"); - - if (isInputFocused) { - return; - } - - // Ignore if any modifier keys are pressed - if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { - return; - } - - if (e.key === "ArrowLeft" && hasPrevious && onPrevious) { - e.preventDefault(); - onPrevious(); - } else if (e.key === "ArrowRight" && hasNext && onNext) { - e.preventDefault(); - onNext(); - } - }; - - // Use capture phase to catch event before it reaches other handlers - document.addEventListener("keydown", handleGlobalKeyDown, true); - - return () => { - document.removeEventListener("keydown", handleGlobalKeyDown, true); - }; - }, [enableKeyboardNav, hasPrevious, hasNext, onPrevious, onNext]); - - return ( -
-
- -
- {children &&
{children}
} -
- -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/Pager.module.css b/src/inspect_scout/_view/www/src/app/components/Pager.module.css deleted file mode 100644 index c2da10087..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Pager.module.css +++ /dev/null @@ -1,29 +0,0 @@ -.pager { - --bs-pagination-padding-x: 0.5em; - --bs-pagination-padding-y: 0.15em; - --bs-pagination-font-size: 0.8rem; - --bs-pagination-padding-x: 0.5em; - --bs-pagination-padding-y: 0; - --bs-pagination-border-radius: var(--bs-border-radius); - margin-bottom: 0; -} - -.item:not(:global(.disabled)) { - cursor: pointer; -} - -.item { - margin-left: 0.2em; - margin-right: 0.2em; -} - -.item:first-child, -.item:last-child { - --bs-pagination-padding-x: 0.3em; -} - -.item:first-child :global(.ii:before), -.item:last-child :global(.ii:before) { - margin-top: -2px; - font-size: 0.8em; -} diff --git a/src/inspect_scout/_view/www/src/app/components/Pager.tsx b/src/inspect_scout/_view/www/src/app/components/Pager.tsx deleted file mode 100644 index 1abdf863d..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Pager.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { ApplicationIcons } from "../../components/icons"; - -import styles from "./Pager.module.css"; - -interface PagerProps { - itemCount: number; - page?: number; - setPage?: (page: number) => void; - itemsPerPage?: number; -} - -export const Pager: FC = ({ - itemCount, - page, - setPage, - itemsPerPage = 20, -}) => { - const pageCount = Math.ceil(itemCount / itemsPerPage); - if (pageCount <= 1) { - return null; - } - - const currentPage = page || 0; - - const generatePaginationSegments = () => { - const segments: Array<{ - type: "page" | "ellipsis"; - page?: number; - key: string; - }> = []; - - if (pageCount <= 5) { - // Show all pages if 5 or fewer - for (let i = 0; i < pageCount; i++) { - segments.push({ type: "page", page: i, key: `page-${i}` }); - } - } else { - // There are more than 5 pages, use ellpsis to constrain the size - - // first page - segments.push({ type: "page", page: 0, key: "page-0" }); - - // Determine the range around current page - const startPage = Math.max(1, currentPage - 1); - const endPage = Math.min(pageCount - 2, currentPage + 1); - - // Add ellipsis before middle section if needed - if (startPage > 1) { - segments.push({ type: "ellipsis", key: "ellipsis-start" }); - } - - // Add middle section pages - for (let i = startPage; i <= endPage; i++) { - segments.push({ type: "page", page: i, key: `page-${i}` }); - } - - // Add ellipsis after middle section if needed - if (endPage < pageCount - 2) { - segments.push({ type: "ellipsis", key: "ellipsis-end" }); - } - - // last page - segments.push({ - type: "page", - page: pageCount - 1, - key: `page-${pageCount - 1}`, - }); - } - - return segments; - }; - - const segments = generatePaginationSegments(); - - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/ProjectBar.module.css b/src/inspect_scout/_view/www/src/app/components/ProjectBar.module.css deleted file mode 100644 index fc249531b..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ProjectBar.module.css +++ /dev/null @@ -1,68 +0,0 @@ -.projectBar { - display: flex; - flex-direction: column; - background: var(--bs-light); - border-bottom: solid 1px var(--bs-border-color); - min-height: 2em; - padding: 0.1rem 0.3rem; - row-gap: 0; - align-items: center; - justify-content: center; -} - -.row { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - column-gap: 0.5rem; - width: 100%; - height: 100%; -} - -.left, -.center, -.right { - font-size: var(--inspect-font-size-small); -} - -.left { - grid-column: 1; - justify-self: start; - display: flex; - align-items: center; - gap: 0.2rem; -} - -.center { - grid-column: 2; - justify-self: center; - display: flex; - align-items: center; -} - -.right { - grid-column: 3; - justify-self: end; - display: flex; - align-items: center; -} - -.historyButton { - font-size: 0.9em; -} - -.navButton { - display: inline-flex; - align-items: center; - justify-content: center; - border: none; - background: transparent; - color: var(--bs-secondary); - padding: 0.12rem 0.3rem; - border-radius: var(--bs-border-radius); -} - -.navButton:hover, -.navButton:focus-visible { - background-color: var(--bs-secondary-bg-subtle); -} diff --git a/src/inspect_scout/_view/www/src/app/components/ProjectBar.tsx b/src/inspect_scout/_view/www/src/app/components/ProjectBar.tsx deleted file mode 100644 index d0ede66cd..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ProjectBar.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { clsx } from "clsx"; -import { FC } from "react"; -import { useLocation } from "react-router-dom"; - -import { ApplicationIcons } from "../../components/icons"; -import { useLoggingNavigate } from "../../debugging/navigationDebugging"; -import { getActivityByRoute } from "../../router/activities"; -import { AppConfig } from "../../types/api-types"; -import { appAliasedPath } from "../server/useAppConfig"; - -import styles from "./ProjectBar.module.css"; - -interface ProjectBarProps { - config: AppConfig; -} - -export const ProjectBar: FC = ({ config }) => { - const navigate = useLoggingNavigate("ProjectBar"); - const location = useLocation(); - const currentActivity = getActivityByRoute(location.pathname); - - return ( -
-
-
- - - -
- - {appAliasedPath(config, config.project_dir)} - -
-
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/ScansNavbar.tsx b/src/inspect_scout/_view/www/src/app/components/ScansNavbar.tsx deleted file mode 100644 index d2b9f98a2..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ScansNavbar.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { FC, ReactNode, useMemo } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { ApplicationIcons } from "../../components/icons"; -import { scanRoute, scansRoute } from "../../router/url"; -import { useStore } from "../../state/store"; -import { dirname } from "../../utils/path"; -import { useScanRoute } from "../hooks/useScanRoute"; - -import { EditablePath } from "./EditablePath"; -import { Navbar } from "./Navbar"; -import { NavButton } from "./NavButtons"; - -interface ScansNavbarProps { - scansDir: string | null; - scansDirSource?: "route" | "user" | "project" | "cli"; - setScansDir: (path: string) => void; - children?: ReactNode; - bordered?: boolean; -} - -export const ScansNavbar: FC = ({ - scansDir, - scansDirSource, - setScansDir, - bordered = true, - children, -}) => { - const { - relativePath, - scanPath, - scanResultUuid, - scansDir: routeScansDir, - } = useScanRoute(); - const singleFileMode = useStore((state) => state.singleFileMode); - const [searchParams] = useSearchParams(); - - // Check if we're on a scan result page and calculate the appropriate back URL - const resolvedScansDir = routeScansDir || scansDir; - - const backUrl = - resolvedScansDir && scanResultUuid - ? scanRoute(resolvedScansDir, scanPath, searchParams) - : !singleFileMode && resolvedScansDir - ? scansRoute(resolvedScansDir, dirname(relativePath || "")) - : undefined; - - const navButtons: NavButton[] = useMemo(() => { - const buttons: NavButton[] = []; - - if (backUrl) { - buttons.push({ - title: "Back", - icon: ApplicationIcons.navbar.back, - route: backUrl, - enabled: !!scanPath, - }); - } - - if (!singleFileMode && resolvedScansDir) { - buttons.push({ - title: "Home", - icon: ApplicationIcons.navbar.home, - route: scansRoute(resolvedScansDir), - enabled: !!scanPath, - }); - } - - return buttons; - }, [backUrl, singleFileMode, scanPath, resolvedScansDir]); - - return ( - 0 ? navButtons : undefined} - left={ - scansDir ? ( - - ) : undefined - } - /> - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/ScoreValue.tsx b/src/inspect_scout/_view/www/src/app/components/ScoreValue.tsx deleted file mode 100644 index 57d6ed585..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ScoreValue.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { MetaDataGrid } from "../../components/content/MetaDataGrid"; -import { JsonValue } from "../../types/api-types"; -import { isRecord } from "../../utils/type"; - -interface ScoreProps { - score: JsonValue; - className?: string | string[]; -} - -export const ScoreValue: FC = ({ score, className }) => { - return
{renderScore(score)}
; -}; - -export const renderScore = (value: JsonValue) => { - if (Array.isArray(value)) { - return value.join(", "); - } else if (isRecord(value) && typeof value === "object") { - return ; - } else { - return String(value); - } -}; diff --git a/src/inspect_scout/_view/www/src/app/components/TaskName.tsx b/src/inspect_scout/_view/www/src/app/components/TaskName.tsx deleted file mode 100644 index 266921d6f..000000000 --- a/src/inspect_scout/_view/www/src/app/components/TaskName.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import clsx from "clsx"; -import { FC, ReactNode } from "react"; - -interface TaskNameProps { - taskSet?: string | null; - taskId?: string | number | null; - taskRepeat?: number | null; -} - -export const TaskName: FC = ({ - taskSet, - taskId, - taskRepeat, -}) => { - if (!taskSet && !taskId && taskRepeat === undefined) { - return ""; - } - - const results: ReactNode[] = []; - - if (taskSet) { - results.push({taskSet}); - } - - if (taskId !== undefined && taskId !== null) { - results.push("/", {taskId}); - } - - if (taskRepeat !== undefined && taskRepeat !== null) { - results.push( - " ", - {`(${taskRepeat})`} - ); - } - - return results; -}; diff --git a/src/inspect_scout/_view/www/src/app/components/TranscriptsNavbar.tsx b/src/inspect_scout/_view/www/src/app/components/TranscriptsNavbar.tsx deleted file mode 100644 index 75b97188f..000000000 --- a/src/inspect_scout/_view/www/src/app/components/TranscriptsNavbar.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { FC, useContext, useMemo } from "react"; -import { useParams, useSearchParams } from "react-router-dom"; - -import { AppModeContext } from "../../App"; -import { ApplicationIcons } from "../../components/icons"; -import { transcriptsRoute } from "../../router/url"; -import { useStore } from "../../state/store"; - -import { EditablePath } from "./EditablePath"; -import { Navbar } from "./Navbar"; -import { NavButton } from "./NavButtons"; - -interface TranscriptsNavbarProps { - transcriptsDir?: string | null; - transcriptsDirSource?: "route" | "user" | "project" | "cli" | "unknown"; - filter?: string; - setTranscriptsDir: (path: string) => void; - bordered?: boolean; - children?: React.ReactNode; -} - -export const TranscriptsNavbar: FC = ({ - transcriptsDir, - transcriptsDirSource, - filter, - setTranscriptsDir, - bordered = true, - children, -}) => { - const appMode = useContext(AppModeContext); - const showNavButtons = appMode !== "workbench"; - const singleFileMode = useStore((state) => state.singleFileMode); - const [searchParams] = useSearchParams(); - - const params = useParams<{ "*": string }>(); - const transcriptId = params["transcriptId"]; - - // Check if we're on a scan result page and calculate the appropriate back URL - const backUrl = !singleFileMode ? transcriptsRoute(searchParams) : undefined; - - const navButtons: NavButton[] = useMemo(() => { - const buttons: NavButton[] = []; - - if (backUrl) { - buttons.push({ - title: "Back", - icon: ApplicationIcons.navbar.back, - route: backUrl, - enabled: !!transcriptId, - }); - } - - if (!singleFileMode) { - buttons.push({ - title: "Home", - icon: ApplicationIcons.navbar.home, - route: transcriptsRoute(), - enabled: !!transcriptId, - }); - } - - return buttons; - // TODO: lint react-hooks/exhaustive-deps Fix this - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [backUrl, singleFileMode]); - - const editable = false; - const filterText = - filter && !filter?.startsWith("(") - ? `(${filter})` - : filter - ? filter - : undefined; - - return ( - - } - right={children} - /> - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/ValidationResult.module.css b/src/inspect_scout/_view/www/src/app/components/ValidationResult.module.css deleted file mode 100644 index b7012677c..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ValidationResult.module.css +++ /dev/null @@ -1,34 +0,0 @@ -.result { - display: inline-flex; - justify-content: center; - align-items: center; - border-radius: 5px; - padding: 8px 4px; - height: 1rem; - font-size: 0.7rem; -} - -.true { - border: solid var(--bs-success) 1px; - color: var(--bs-success); -} - -.false { - border: solid var(--bs-danger) 1px; - color: var(--bs-danger); -} - -.resultContainer { - display: grid; - grid-template-columns: max-content max-content; - align-items: center; - column-gap: 0.2rem; -} - -.targetValue { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - border: none; - margin-left: 0.4rem; -} diff --git a/src/inspect_scout/_view/www/src/app/components/ValidationResult.tsx b/src/inspect_scout/_view/www/src/app/components/ValidationResult.tsx deleted file mode 100644 index 27211f53c..000000000 --- a/src/inspect_scout/_view/www/src/app/components/ValidationResult.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { ApplicationIcons } from "../../components/icons"; -import { JsonValue } from "../../types/json-value"; - -import styles from "./ValidationResult.module.css"; - -interface ValidationResultProps { - result: boolean | Record; - target?: JsonValue; - label?: string; -} - -export const ValidationResult: FC = ({ - result, - target, - label, -}) => { - // TODO: stringify the target value properly - if (typeof result === "boolean") { - return ( - - ); - } else if (result !== null && typeof result === "object") { - const entries = Object.entries(result); - - return ( -
- {entries.map(([key, value]) => ( -
- -
- ))} -
- ); - } -}; - -const Result: FC<{ value: boolean; targetValue?: string }> = ({ - value, - targetValue, -}) => { - return ( -
-
- {value ? ( - - ) : ( - - )} -
- - {targetValue} - -
- ); -}; - -const resolveTargetValue = (target?: JsonValue, key?: string): JsonValue => { - if (target === undefined) { - return ""; - } - - if (key === undefined) { - return target; - } - - if (target && typeof target === "object" && !Array.isArray(target)) { - return (target as Record)[key] || false; - } - return target; -}; - -const valueStr = (target?: JsonValue): string => { - if (target === null) { - return "null"; - } else if (typeof target === "string") { - return target; - } else if (typeof target === "number" || typeof target === "boolean") { - return target.toString(); - } else if (Array.isArray(target)) { - return `[Array(${target.length})]`; - } else if (typeof target === "object") { - return "{Object}"; - } else { - return "undefined"; - } -}; diff --git a/src/inspect_scout/_view/www/src/app/components/Value.module.css b/src/inspect_scout/_view/www/src/app/components/Value.module.css deleted file mode 100644 index 637d10818..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Value.module.css +++ /dev/null @@ -1,53 +0,0 @@ -.boolean { - display: inline-flex; - justify-content: center; - align-items: center; - border-radius: 5px; - padding: 8px; - margin-bottom: 1px; - width: 2.5rem; - height: 1rem; - font-size: var(--inspect-font-size-smallest); -} - -.true { - background-color: var(--bs-success); - border: solid var(--bs-success) 1px; - color: var(--bs-body-bg); -} - -.false { - background-color: var(--bs-danger); - border: solid var(--bs-danger) 1px; - color: var(--bs-body-bg); -} - -.valueTable { - display: grid; - grid-template-columns: minmax(auto, max-content) auto; - column-gap: 1rem; - row-gap: 0rem; -} - -.valueKey { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.inline .valueValue { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-align: end; -} - -.inline .valueValue p { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -pre.value { - margin: 0; -} diff --git a/src/inspect_scout/_view/www/src/app/components/Value.tsx b/src/inspect_scout/_view/www/src/app/components/Value.tsx deleted file mode 100644 index ee0737fe3..000000000 --- a/src/inspect_scout/_view/www/src/app/components/Value.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import clsx from "clsx"; -import { FC, Fragment, ReactNode } from "react"; - -import { RecordTree } from "../../components/content/RecordTree"; -import { - MarkdownDivWithReferences, - MarkdownReference, -} from "../../components/MarkdownDivWithReferences"; -import { printArray } from "../../utils/array"; -import { formatPrettyDecimal } from "../../utils/format"; -import { printObject } from "../../utils/object"; -import { - ScanResultSummary, - isStringValue, - isNumberValue, - isBooleanValue, - isNullValue, - isArrayValue, - isObjectValue, -} from "../types"; - -import styles from "./Value.module.css"; - -interface ValueProps { - summary: ScanResultSummary; - references: MarkdownReference[]; - style: "inline" | "block"; - maxTableSize?: number; - interactive?: boolean; - options?: { - previewRefsOnHover?: boolean; - }; -} - -// TODO: Implement popover viewer for object and list values -export const Value: FC = ({ - summary: result, - references, - style, - maxTableSize = 5, - interactive = false, - options, -}): ReactNode => { - if (isStringValue(result)) { - return ( - - ); - } else if (isNumberValue(result) && result.value !== null) { - return formatPrettyDecimal(result.value); - } else if (isBooleanValue(result)) { - return ( -
- {String(result.value)} -
- ); - } else if (isNullValue(result)) { - return null; - } else if (isArrayValue(result)) { - return ( - - ); - } else if (isObjectValue(result)) { - return ( - - ); - } else { - return "Unknown value type"; - } -}; - -const ValueList: FC<{ - value: unknown[]; - summary: ScanResultSummary; - maxListSize: number; - interactive: boolean; - references: MarkdownReference[]; - style: "inline" | "block"; -}> = ({ - value, - summary: result, - maxListSize, - interactive, - references, - style, -}) => { - // Display only maxListSize rows - const itemsToDisplay = value.slice(0, maxListSize); - - // Display the rows - return ( -
- {itemsToDisplay.map((item, index) => { - const displayValue = renderValue( - index, - item, - result, - references, - interactive - ); - return ( - -
- [{index}] -
-
{displayValue}
-
- ); - })} -
- ); -}; - -const ValueTable: FC<{ - value: object; - summary: ScanResultSummary; - maxTableSize: number; - interactive: boolean; - references: MarkdownReference[]; - style: "inline" | "block"; -}> = ({ - value, - summary: result, - maxTableSize, - interactive, - references, - style, -}) => { - // Sort keys by the value (desc, so true to false), then slice 5 keys to display - const sortedKeys = Object.keys(value).sort((a, b) => { - const aVal = (value as Record)[a]; - const bVal = (value as Record)[b]; - if (typeof aVal === "boolean" && typeof bVal === "boolean") { - return Number(bVal) - Number(aVal); - } else if (typeof aVal === "number" && typeof bVal === "number") { - return bVal - aVal; - } else { - // Keep original order if not boolean - return 0; - } - }); - - // Display only 5 rows - const keysToDisplay = sortedKeys.slice(0, maxTableSize); - const notShown = Object.keys(value).length - maxTableSize; - - // Display the rows - return ( -
- {keysToDisplay.map((key, index) => { - const displayValue = renderValue( - index, - (value as Record)[key], - result, - references, - interactive - ); - return ( - -
- {key} -
-
{displayValue}
-
- ); - })} - {notShown > 0 && ( - -
- {notShown} more… -
-
-
- )} -
- ); -}; - -// Renders a simple value (don't further render objects or lists here) -const renderValue = ( - index: number, - val: unknown, - summary: ScanResultSummary, - references: MarkdownReference[], - interactive: boolean -): ReactNode => { - if (typeof val === "string") { - return ; - } else if (typeof val === "number") { - return formatPrettyDecimal(val); - } else if (typeof val === "boolean") { - return ( -
- {String(val)} -
- ); - } else if (val === null) { - return
null
; - } else if (Array.isArray(val)) { - return printArray(val, 25); - } else if (typeof val === "object") { - return !interactive ? ( - printObject(val, 35) - ) : ( - } - /> - ); - } else { - return "Unknown value type"; - } -}; diff --git a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterButton.module.css b/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterButton.module.css deleted file mode 100644 index fa8139b0d..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterButton.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.filterButton { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 2px; - border: none; - background: transparent; - color: var(--bs-secondary); - cursor: pointer; -} - -.filterButtonActive { - color: var(--bs-primary); -} diff --git a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterButton.tsx b/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterButton.tsx deleted file mode 100644 index 851ff55bf..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import clsx from "clsx"; -import { forwardRef } from "react"; - -import { ApplicationIcons } from "../../../components/icons"; - -import styles from "./ColumnFilterButton.module.css"; - -export interface ColumnFilterButtonProps { - columnId: string; - isActive: boolean; - onClick: (event: React.MouseEvent) => void; -} - -export const ColumnFilterButton = forwardRef< - HTMLButtonElement, - ColumnFilterButtonProps ->(({ columnId, isActive, onClick }, ref) => { - return ( - - ); -}); - -ColumnFilterButton.displayName = "ColumnFilterButton"; diff --git a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterControl.module.css b/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterControl.module.css deleted file mode 100644 index 48c4e310b..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterControl.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.headerActions { - display: flex; - align-items: center; - gap: 4px; - margin-right: 6px; -} - -.filterPopover { - min-width: 180px; -} diff --git a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterControl.tsx b/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterControl.tsx deleted file mode 100644 index 7349ff967..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterControl.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { FC, useCallback, useRef } from "react"; - -import { ScalarValue } from "../../../api/api"; -import { PopOver } from "../../../components/PopOver"; -import type { SimpleCondition } from "../../../query/types"; -import type { FilterType } from "../../../state/store"; - -import { ColumnFilterButton } from "./ColumnFilterButton"; -import styles from "./ColumnFilterControl.module.css"; -import { ColumnFilterEditor } from "./ColumnFilterEditor"; -import { useColumnFilterPopover } from "./useColumnFilterPopover"; - -interface ColumnFilterControlProps { - columnId: string; - filterType: FilterType; - condition: SimpleCondition | null; - onChange: (condition: SimpleCondition | null) => void; - /** Autocomplete suggestions for the filter value */ - suggestions?: ScalarValue[]; - /** Called when the popover opens/closes (for fetching suggestions) */ - onOpenChange?: (columnId: string | null) => void; -} - -export const ColumnFilterControl: FC = ({ - columnId, - filterType, - condition, - onChange, - suggestions = [], - onOpenChange, -}) => { - const buttonRef = useRef(null); - - const { - isOpen, - setIsOpen, - operator, - setOperator, - operatorOptions, - value: rawValue, - setValue: setRawValue, - value2: rawValue2, - setValue2: setRawValue2, - isValueDisabled, - isRangeOperator, - commitAndClose, - cancelAndClose, - } = useColumnFilterPopover({ - columnId, - filterType, - condition, - onChange, - }); - - const handlePopoverOpenChange = useCallback( - (nextOpen: boolean) => { - setIsOpen(nextOpen); - onOpenChange?.(nextOpen ? columnId : null); - }, - [setIsOpen, onOpenChange, columnId] - ); - - return ( -
- { - event.stopPropagation(); - handlePopoverOpenChange(!isOpen); - }} - /> - - - -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterEditor.module.css b/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterEditor.module.css deleted file mode 100644 index a60fa18f8..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterEditor.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.filterContent { - display: flex; - flex-direction: column; - gap: 8px; -} - -.filterRow { - display: flex; - align-items: center; - gap: 8px; -} - -.filterSelect, -.filterInput { - flex: 1; - font-size: 12px; - padding: 4px 6px; - border-radius: 4px; - border: 1px solid var(--bs-border-color); - background: var(--bs-body-bg); - color: var(--bs-body-color); -} - -.filterInput:disabled, -.filterSelect:disabled { - background: var(--bs-light); - color: var(--bs-secondary); -} - -.columnId { - font-weight: 600; - font-family: var(--bs-monospace); -} - -.columnIdText { - margin-left: 0.5rem; -} - -:global(.btn).filterButton { - width: 100%; - padding: 0.2rem; -} - -.durationInputWrapper { - display: flex; - flex-direction: column; - gap: 4px; - flex: 1; -} - -.durationHelper { - font-size: 11px; - color: var(--bs-secondary); - padding-left: 2px; -} - -.rangeLabel { - font-size: 11px; - color: var(--bs-secondary); - padding-left: 2px; -} diff --git a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterEditor.tsx b/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterEditor.tsx deleted file mode 100644 index 2ff1c01c2..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnFilter/ColumnFilterEditor.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import clsx from "clsx"; -import { ChangeEvent, FC, KeyboardEvent, useCallback } from "react"; - -import { ScalarValue } from "../../../api/api"; -import { AutocompleteInput } from "../../../components/AutocompleteInput"; -import type { OperatorModel } from "../../../query"; -import type { FilterType } from "../../../state/store"; - -import styles from "./ColumnFilterEditor.module.css"; -import { DurationInput } from "./DurationInput"; - -export interface AvailableColumn { - id: string; - label: string; - filterType: FilterType; -} - -export interface ColumnFilterEditorProps { - columnId: string; - filterType: FilterType; - operator: OperatorModel; - operatorOptions: OperatorModel[]; - rawValue: string; - /** Second value for BETWEEN/NOT BETWEEN operators */ - rawValue2?: string; - isValueDisabled: boolean; - /** True if operator expects a range with two values (BETWEEN, NOT BETWEEN) */ - isRangeOperator?: boolean; - onOperatorChange: (operator: OperatorModel) => void; - onValueChange: (value: string) => void; - /** Handler for second value changes (BETWEEN operators) */ - onValue2Change?: (value: string) => void; - onCommit?: () => void; - onCancel?: () => void; - suggestions?: ScalarValue[]; - // Add mode props - mode?: "add" | "edit"; - columns?: AvailableColumn[]; - onColumnChange?: (columnId: string) => void; -} - -export const ColumnFilterEditor: FC = ({ - columnId, - filterType, - operator, - operatorOptions, - rawValue, - rawValue2 = "", - isValueDisabled, - isRangeOperator = false, - onOperatorChange, - onValueChange, - onValue2Change, - onCommit, - onCancel, - suggestions = [], - mode = "edit", - columns, - onColumnChange, -}) => { - const isAddMode = mode === "add"; - const hasColumnSelected = isAddMode ? !!columnId : true; - // In add mode, show dropdown only until a column is selected - const showColumnDropdown = isAddMode && !columnId; - - // Get the display label for the selected column - const selectedColumnLabel = columnId - ? (columns?.find((col) => col.id === columnId)?.label ?? columnId) - : ""; - - const handleColumnSelectChange = useCallback( - (event: ChangeEvent) => { - onColumnChange?.(event.target.value); - }, - [onColumnChange] - ); - const handleOperatorChange = useCallback( - (event: ChangeEvent) => { - const operator = event.target.value as OperatorModel; - onOperatorChange(operator); - }, - [onOperatorChange] - ); - - const handleValueChange = useCallback( - (event: ChangeEvent) => { - const value = event.target.value; - onValueChange(value); - }, - [onValueChange] - ); - - const handleValue2Change = useCallback( - (event: ChangeEvent) => { - const value = event.target.value; - onValue2Change?.(value); - }, - [onValue2Change] - ); - - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { - // Prevent parent bubbling - event.stopPropagation(); - - if (event.key === "Escape") { - event.preventDefault(); - onCancel?.(); - } - if (event.key === "Enter") { - event.preventDefault(); - onCommit?.(); - } - }, - [onCancel, onCommit] - ); - - return ( -
- {/* Column row - dropdown until selected, then static text */} -
- {showColumnDropdown ? ( - - ) : ( - selectedColumnLabel - )} -
- - {/* Operator and value rows - only show when column is selected */} - {hasColumnSelected && ( - <> -
- -
- {isRangeOperator && Start} -
- {filterType === "boolean" ? ( - - ) : filterType === "string" || filterType === "unknown" ? ( - - ) : filterType === "duration" ? ( - - ) : ( - - )} -
- {/* Second input for BETWEEN/NOT BETWEEN operators */} - {isRangeOperator && ( - <> - End -
- {filterType === "duration" ? ( - - ) : ( - - )} -
- - )} - - )} - -
- -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/columnFilter/DurationInput.tsx b/src/inspect_scout/_view/www/src/app/components/columnFilter/DurationInput.tsx deleted file mode 100644 index 7e20580e0..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnFilter/DurationInput.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { ChangeEvent, FC, useMemo } from "react"; - -import { formatTime } from "../../../utils/format"; - -import styles from "./ColumnFilterEditor.module.css"; - -export interface DurationInputProps { - id: string; - value: string; - onChange: (event: ChangeEvent) => void; - disabled?: boolean; - autoFocus?: boolean; -} - -export const DurationInput: FC = ({ - id, - value, - onChange, - disabled, - autoFocus, -}) => { - const parsedSeconds = useMemo(() => { - const num = Number(value); - return Number.isFinite(num) && num >= 0 ? num : null; - }, [value]); - - return ( -
- - {parsedSeconds !== null && ( -
{formatTime(parsedSeconds)}
- )} -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/components/columnFilter/index.ts b/src/inspect_scout/_view/www/src/app/components/columnFilter/index.ts deleted file mode 100644 index c518737cc..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnFilter/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -export { ColumnFilterControl } from "./ColumnFilterControl"; -export { ColumnFilterButton } from "./ColumnFilterButton"; -export { ColumnFilterEditor } from "./ColumnFilterEditor"; -export { DurationInput } from "./DurationInput"; -export { useColumnFilter } from "./useColumnFilter"; -export { useColumnFilterPopover } from "./useColumnFilterPopover"; -export { useAddFilterPopover } from "./useAddFilterPopover"; -export type { - UseColumnFilterParams, - UseColumnFilterReturn, -} from "./useColumnFilter"; -export type { - ColumnFilterEditorProps, - AvailableColumn, -} from "./ColumnFilterEditor"; -export type { ColumnFilterButtonProps } from "./ColumnFilterButton"; -export type { - UseColumnFilterPopoverParams, - UseColumnFilterPopoverReturn, -} from "./useColumnFilterPopover"; -export type { UseAddFilterPopoverParams } from "./useAddFilterPopover"; diff --git a/src/inspect_scout/_view/www/src/app/components/columnFilter/useAddFilterPopover.ts b/src/inspect_scout/_view/www/src/app/components/columnFilter/useAddFilterPopover.ts deleted file mode 100644 index aeaa81368..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnFilter/useAddFilterPopover.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -import type { OperatorModel } from "../../../query"; -import type { ColumnFilter } from "../../../state/store"; - -import type { AvailableColumn } from "./ColumnFilterEditor"; -import { useColumnFilter } from "./useColumnFilter"; - -export interface UseAddFilterPopoverParams { - /** Columns available for filtering */ - columns: AvailableColumn[]; - /** Current filters */ - filters: Record; - /** Callback when a filter is added */ - onAddFilter: (filter: ColumnFilter) => void; - /** Callback when the selected column changes (for fetching suggestions) */ - onFilterColumnChange?: (columnId: string | null) => void; -} - -/** - * Generic hook for managing the "Add Filter" popover state. - * Wraps useColumnFilter with column selection and open/close logic. - */ -export function useAddFilterPopover({ - columns, - filters, - onAddFilter, - onFilterColumnChange, -}: UseAddFilterPopoverParams) { - const [isOpen, setIsOpen] = useState(false); - const [selectedColumnId, setSelectedColumnId] = useState(null); - const prevOpenRef = useRef(false); - - const selectedColumn = selectedColumnId - ? columns.find((c) => c.id === selectedColumnId) - : null; - const filterType = selectedColumn?.filterType ?? "string"; - - const existingFilter = selectedColumnId ? filters[selectedColumnId] : null; - - const { - operator, - setOperator, - value, - setValue, - value2, - setValue2, - operatorOptions, - usesValue: isValueDisabled, - usesRangeValue: isRangeOperator, - buildCondition, - } = useColumnFilter({ - columnId: selectedColumnId ?? "", - filterType, - condition: existingFilter?.condition ?? null, - isOpen, - }); - - // Reset column selection when popover opens - useEffect(() => { - if (isOpen && !prevOpenRef.current) { - // TODO: lint react-hooks/set-state-in-effect - consider if fixing this violation makes sense - // eslint-disable-next-line react-hooks/set-state-in-effect - setSelectedColumnId(null); - } - prevOpenRef.current = isOpen; - }, [isOpen]); - - // Notify parent when popover closes - useEffect(() => { - if (prevOpenRef.current && !isOpen) { - onFilterColumnChange?.(null); - } - }, [isOpen, onFilterColumnChange]); - - const handleColumnChange = useCallback( - (newColumnId: string) => { - setSelectedColumnId(newColumnId || null); - if (newColumnId) { - onFilterColumnChange?.(newColumnId); - } - }, - [onFilterColumnChange] - ); - - const commitAndClose = useCallback(() => { - if (!selectedColumnId) return; - - const condition = buildCondition(operator, value, value2); - if (condition === undefined) return; - - onAddFilter({ columnId: selectedColumnId, filterType, condition }); - setIsOpen(false); - }, [ - selectedColumnId, - buildCondition, - operator, - value, - value2, - onAddFilter, - filterType, - ]); - - const cancelAndClose = useCallback(() => setIsOpen(false), []); - - return { - isOpen, - setIsOpen, - selectedColumnId, - columns, - filterType, - operator, - setOperator: setOperator as (op: OperatorModel) => void, - operatorOptions, - value, - setValue, - value2, - setValue2, - isValueDisabled, - isRangeOperator, - handleColumnChange, - commitAndClose, - cancelAndClose, - }; -} diff --git a/src/inspect_scout/_view/www/src/app/components/columnFilter/useColumnFilter.ts b/src/inspect_scout/_view/www/src/app/components/columnFilter/useColumnFilter.ts deleted file mode 100644 index 7d8c2995e..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnFilter/useColumnFilter.ts +++ /dev/null @@ -1,387 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -import { ConditionBuilder } from "../../../query"; -import type { OperatorModel, ScalarValue } from "../../../query"; -import type { SimpleCondition } from "../../../query/types"; -import type { FilterType } from "../../../state/store"; -import { - formatDateForInput, - formatDateTimeForInput, - parseDateFromInput, -} from "../../../utils/date"; - -const OPERATORS_BY_TYPE: Record = { - string: [ - "=", - "!=", - "LIKE", - "NOT LIKE", - "ILIKE", - "NOT ILIKE", - "IN", - "NOT IN", - "IS NULL", - "IS NOT NULL", - ], - number: [ - "=", - "!=", - "<", - "<=", - ">", - ">=", - "IN", - "NOT IN", - "BETWEEN", - "NOT BETWEEN", - "IS NULL", - "IS NOT NULL", - ], - boolean: ["=", "!=", "IS NULL", "IS NOT NULL"], - date: [ - "=", - "!=", - "<", - "<=", - ">", - ">=", - "BETWEEN", - "NOT BETWEEN", - "IS NULL", - "IS NOT NULL", - ], - datetime: [ - "=", - "!=", - "<", - "<=", - ">", - ">=", - "BETWEEN", - "NOT BETWEEN", - "IS NULL", - "IS NOT NULL", - ], - duration: [ - "=", - "!=", - "<", - "<=", - ">", - ">=", - "BETWEEN", - "NOT BETWEEN", - "IS NULL", - "IS NOT NULL", - ], - unknown: [ - "=", - "!=", - "LIKE", - "NOT LIKE", - "ILIKE", - "NOT ILIKE", - "IN", - "NOT IN", - "IS NULL", - "IS NOT NULL", - ], -}; - -const OPERATORS_WITHOUT_VALUE = new Set([ - "IS NULL", - "IS NOT NULL", -]); - -const OPERATORS_WITH_LIST_VALUE = new Set(["IN", "NOT IN"]); - -const OPERATORS_WITH_RANGE_VALUE = new Set([ - "BETWEEN", - "NOT BETWEEN", -]); - -/** - * Formats a single scalar value for display in an input field. - */ -const formatScalarValue = ( - value: ScalarValue, - filterType?: FilterType -): string => { - if (value === null || value === undefined) { - return ""; - } - // For date/datetime types, ensure ISO format for native inputs - if (filterType === "date" && typeof value !== "boolean") { - return formatDateForInput(value); - } - if (filterType === "datetime" && typeof value !== "boolean") { - return formatDateTimeForInput(value); - } - return String(value); -}; - -/** - * Formats a filter value (single, array, or tuple) for the primary input field. - * For BETWEEN operators, this returns only the first value. - */ -const formatFilterValue = ( - value: SimpleCondition["right"] | undefined, - filterType?: FilterType -): string => { - if (value === null || value === undefined) { - return ""; - } - // For arrays (IN/NOT IN or BETWEEN tuple), join with comma for IN/NOT IN - // or return first value for BETWEEN - if (Array.isArray(value)) { - // Tuple for BETWEEN - return first value only - if (value.length === 2) { - return formatScalarValue(value[0], filterType); - } - // Array for IN/NOT IN - join with comma - return value.map((v) => formatScalarValue(v, filterType)).join(", "); - } - return formatScalarValue(value, filterType); -}; - -/** - * Formats the second value for BETWEEN operators. - */ -const formatFilterValue2 = ( - value: SimpleCondition["right"] | undefined, - filterType?: FilterType -): string => { - if (value === null || value === undefined) { - return ""; - } - // For BETWEEN tuple, return second value - if (Array.isArray(value) && value.length === 2) { - return formatScalarValue(value[1], filterType); - } - return ""; -}; - -const parseFilterValue = ( - filterType: FilterType, - rawValue: string -): ScalarValue | undefined => { - switch (filterType) { - case "number": - case "duration": { - const parsed = Number(rawValue); - return Number.isFinite(parsed) ? parsed : undefined; - } - case "boolean": - if (rawValue === "true") return true; - if (rawValue === "false") return false; - return undefined; - case "date": - case "datetime": - return parseDateFromInput(rawValue); - case "unknown": - case "string": - default: - return rawValue; - } -}; - -/** - * Parses a comma-separated string into an array of scalar values for IN/NOT IN operators. - */ -const parseListValue = ( - filterType: FilterType, - rawValue: string -): ScalarValue[] | undefined => { - const parts = rawValue - .split(",") - .map((s) => s.trim()) - .filter((s) => s !== ""); - if (parts.length === 0) { - return undefined; - } - const parsed: ScalarValue[] = []; - for (const part of parts) { - const value = parseFilterValue(filterType, part); - if (value === undefined) { - return undefined; - } - parsed.push(value); - } - return parsed; -}; - -/** - * Parses two values into a tuple for BETWEEN/NOT BETWEEN operators. - */ -const parseRangeValue = ( - filterType: FilterType, - rawValue1: string, - rawValue2: string -): [ScalarValue, ScalarValue] | undefined => { - const parsed1 = parseFilterValue(filterType, rawValue1); - const parsed2 = parseFilterValue(filterType, rawValue2); - if (parsed1 === undefined || parsed2 === undefined) { - return undefined; - } - return [parsed1, parsed2]; -}; - -export interface UseColumnFilterParams { - columnId: string; - filterType: FilterType; - condition: SimpleCondition | null; - isOpen: boolean; -} - -export interface UseColumnFilterReturn { - operator: OperatorModel; - setOperator: (operator: OperatorModel) => void; - operatorOptions: OperatorModel[]; - value: string; - setValue: (value: string) => void; - /** Second value for BETWEEN/NOT BETWEEN operators */ - value2: string; - setValue2: (value: string) => void; - /** True if operator requires no value (IS NULL, IS NOT NULL) */ - usesValue: boolean; - /** True if operator expects a list of values (IN, NOT IN) */ - usesListValue: boolean; - /** True if operator expects a range with two values (BETWEEN, NOT BETWEEN) */ - usesRangeValue: boolean; - buildCondition: ( - operator: OperatorModel, - value: string, - value2?: string - ) => SimpleCondition | null | undefined; -} - -export function useColumnFilter({ - columnId, - filterType, - condition, - isOpen, -}: UseColumnFilterParams): UseColumnFilterReturn { - // operators - const operatorOptions = OPERATORS_BY_TYPE[filterType]; - const defaultOperator: OperatorModel = operatorOptions[0] ?? "="; - const [operator, setOperator] = useState( - condition?.operator ?? defaultOperator - ); - - // value (primary) - const [value, setValue] = useState( - formatFilterValue(condition?.right, filterType) - ); - // value2 (secondary for BETWEEN operators) - const [value2, setValue2] = useState( - formatFilterValue2(condition?.right, filterType) - ); - - const isValueDisabled = OPERATORS_WITHOUT_VALUE.has(operator); - const usesListValue = OPERATORS_WITH_LIST_VALUE.has(operator); - const usesRangeValue = OPERATORS_WITH_RANGE_VALUE.has(operator); - - const valueSelectRef = useRef(null); - const valueInputRef = useRef(null); - - // Track the previous columnId to detect when we switch to a different filter - const prevColumnIdRef = useRef(columnId); - - // Sync state when closed OR when switching to a different column while opening - useEffect(() => { - const columnChanged = prevColumnIdRef.current !== columnId; - prevColumnIdRef.current = columnId; - - if (!isOpen || columnChanged) { - // TODO: lint react-hooks/set-state-in-effect - consider if fixing this violation makes sense - /* eslint-disable react-hooks/set-state-in-effect */ - setOperator(condition?.operator ?? defaultOperator); - setValue(formatFilterValue(condition?.right, filterType)); - setValue2(formatFilterValue2(condition?.right, filterType)); - /* eslint-enable react-hooks/set-state-in-effect */ - } - }, [condition, defaultOperator, filterType, isOpen, columnId, setValue]); - - // auto-focus value input when opened - useEffect(() => { - if (!isOpen || isValueDisabled) { - return; - } - const timer = window.setTimeout(() => { - if (filterType === "boolean") { - valueSelectRef.current?.focus(); - } else { - valueInputRef.current?.focus(); - } - }, 0); - return () => window.clearTimeout(timer); - }, [filterType, isOpen, isValueDisabled]); - - const buildCondition = useCallback( - (operator: OperatorModel, value: string, value2?: string) => { - if (OPERATORS_WITHOUT_VALUE.has(operator)) { - return ConditionBuilder.simple(columnId, operator, null); - } - if (value.trim() === "") { - return null; - } - - // Handle list operators (IN, NOT IN) - if (OPERATORS_WITH_LIST_VALUE.has(operator)) { - const parsed = parseListValue(filterType, value); - if (parsed === undefined) { - return undefined; - } - return ConditionBuilder.simple(columnId, operator, parsed); - } - - // Handle range operators (BETWEEN, NOT BETWEEN) - if (OPERATORS_WITH_RANGE_VALUE.has(operator)) { - if (!value2 || value2.trim() === "") { - return null; - } - const parsed = parseRangeValue(filterType, value, value2); - if (parsed === undefined) { - return undefined; - } - return ConditionBuilder.simple(columnId, operator, parsed); - } - - const parsed = parseFilterValue(filterType, value); - if (parsed === undefined) { - return undefined; - } - - // Special case - inject wildcards if not specified - if ( - operator === "LIKE" || - operator === "NOT LIKE" || - operator === "ILIKE" || - operator === "NOT ILIKE" - ) { - let modified = String(parsed); - if (!modified.includes("%")) { - modified = `%${modified}%`; - } - return ConditionBuilder.simple(columnId, operator, modified); - } else { - return ConditionBuilder.simple(columnId, operator, parsed); - } - }, - [columnId, filterType] - ); - - return { - operator, - setOperator, - value, - setValue, - value2, - setValue2, - operatorOptions, - usesValue: isValueDisabled, - usesListValue, - usesRangeValue, - buildCondition, - }; -} diff --git a/src/inspect_scout/_view/www/src/app/components/columnFilter/useColumnFilterPopover.ts b/src/inspect_scout/_view/www/src/app/components/columnFilter/useColumnFilterPopover.ts deleted file mode 100644 index d23f11b05..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnFilter/useColumnFilterPopover.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -import type { SimpleCondition } from "../../../query/types"; -import type { FilterType } from "../../../state/store"; - -import { useColumnFilter } from "./useColumnFilter"; - -export interface UseColumnFilterPopoverParams { - columnId: string; - filterType: FilterType; - condition: SimpleCondition | null; - onChange: (condition: SimpleCondition | null) => void; -} - -export interface UseColumnFilterPopoverReturn { - // Popover state - isOpen: boolean; - setIsOpen: (open: boolean) => void; - - // Filter state from useColumnFilter - operator: ReturnType["operator"]; - setOperator: ReturnType["setOperator"]; - operatorOptions: ReturnType["operatorOptions"]; - - // Value - value: ReturnType["value"]; - setValue: ReturnType["setValue"]; - value2: ReturnType["value2"]; - setValue2: ReturnType["setValue2"]; - isValueDisabled: ReturnType["usesValue"]; - isRangeOperator: ReturnType["usesRangeValue"]; - - // Actions - commitAndClose: () => void; - cancelAndClose: () => void; -} - -export function useColumnFilterPopover({ - columnId, - filterType, - condition, - onChange, -}: UseColumnFilterPopoverParams): UseColumnFilterPopoverReturn { - const [isOpen, setIsOpen] = useState(false); - const cancelRef = useRef(false); - const prevOpenRef = useRef(false); - - const { - operator, - setOperator, - value, - setValue, - value2, - setValue2, - operatorOptions, - usesValue: isValueDisabled, - usesRangeValue: isRangeOperator, - buildCondition, - } = useColumnFilter({ - columnId, - filterType, - condition, - isOpen, - }); - - const cancelAndClose = useCallback(() => { - cancelRef.current = true; - setIsOpen(false); - }, []); - - const commitAndClose = useCallback(() => { - const nextCondition = buildCondition(operator, value, value2); - if (nextCondition === undefined) { - return; - } - setIsOpen(false); - }, [buildCondition, operator, value, value2]); - - // commit changes when popover closes (unless cancelled) - useEffect(() => { - if (prevOpenRef.current && !isOpen) { - if (cancelRef.current) { - cancelRef.current = false; - } else { - const nextCondition = buildCondition(operator, value, value2); - if (nextCondition !== undefined) { - onChange(nextCondition); - } - } - } - prevOpenRef.current = isOpen; - }, [buildCondition, isOpen, onChange, operator, value, value2]); - - return { - isOpen, - setIsOpen, - operator, - setOperator, - value, - setValue, - value2, - setValue2, - operatorOptions, - isValueDisabled, - isRangeOperator, - commitAndClose, - cancelAndClose, - }; -} diff --git a/src/inspect_scout/_view/www/src/app/components/columnSizing/defaultStrategy.ts b/src/inspect_scout/_view/www/src/app/components/columnSizing/defaultStrategy.ts deleted file mode 100644 index 9089d00f7..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnSizing/defaultStrategy.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Default sizing strategy - uses the column's defined `size` property. - */ - -import { ColumnSizingState } from "@tanstack/react-table"; - -import { getColumnId, SizingStrategy } from "./types"; - -export const defaultStrategy: SizingStrategy = { - computeSizes({ columns }): ColumnSizingState { - const sizing: ColumnSizingState = {}; - - for (const column of columns) { - const id = getColumnId(column); - if (id && column.size !== undefined) { - sizing[id] = column.size; - } - } - - return sizing; - }, -}; diff --git a/src/inspect_scout/_view/www/src/app/components/columnSizing/fitContentStrategy.ts b/src/inspect_scout/_view/www/src/app/components/columnSizing/fitContentStrategy.ts deleted file mode 100644 index 4c9118eed..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnSizing/fitContentStrategy.ts +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Fit-content sizing strategy - measures content and sizes columns to fit. - */ - -import { ColumnSizingState } from "@tanstack/react-table"; - -import { clampSize, DEFAULT_SIZE, getColumnId, SizingStrategy } from "./types"; - -/** - * Measure text width using a temporary span element. - */ -function measureTextWidth( - text: string, - font: string, - measureContainer: HTMLElement -): number { - const span = document.createElement("span"); - span.style.cssText = `white-space: nowrap; font: ${font}; visibility: hidden; position: absolute;`; - span.textContent = text; - measureContainer.appendChild(span); - const width = span.offsetWidth; - measureContainer.removeChild(span); - return width; -} - -/** - * Measure the extra width needed for header elements. - */ -function measureHeaderExtraWidth(tableElement: HTMLTableElement): number { - const headerCell = tableElement.querySelector("th"); - if (!headerCell) return 40; - - const headerStyle = getComputedStyle(headerCell); - const paddingLeft = parseFloat(headerStyle.paddingLeft) || 0; - const paddingRight = parseFloat(headerStyle.paddingRight) || 0; - const gap = parseFloat(headerStyle.gap) || 0; - - const filterButton = headerCell.querySelector("button"); - const filterButtonWidth = filterButton ? filterButton.offsetWidth : 0; - - const sortIcon = headerCell.querySelector("i"); - const sortIconWidth = sortIcon - ? sortIcon.offsetWidth + 4 - : parseFloat(headerStyle.fontSize) || 12; - - return ( - paddingLeft + paddingRight + gap * 2 + filterButtonWidth + sortIconWidth - ); -} - -/** - * Measure the horizontal padding of table cells. - */ -function measureCellPadding(cellElement: HTMLTableCellElement | null): number { - if (!cellElement) return 16; - - const cellStyle = getComputedStyle(cellElement); - const paddingLeft = parseFloat(cellStyle.paddingLeft) || 0; - const paddingRight = parseFloat(cellStyle.paddingRight) || 0; - - return paddingLeft + paddingRight; -} - -export const fitContentStrategy: SizingStrategy = { - computeSizes({ - tableElement, - columns, - data, - constraints, - }): ColumnSizingState { - const sizing: ColumnSizingState = {}; - - // Fall back to default sizes if no table element or data - if (!tableElement || data.length === 0) { - for (const column of columns) { - const id = getColumnId(column); - if (id) { - sizing[id] = column.size ?? DEFAULT_SIZE; - } - } - return sizing; - } - - // Create a hidden container for measurement - const measureContainer = document.createElement("div"); - measureContainer.style.cssText = - "position: absolute; visibility: hidden; pointer-events: none; top: -9999px;"; - document.body.appendChild(measureContainer); - - try { - const headerElement = tableElement.querySelector("th"); - const cellElement = tableElement.querySelector("td"); - - const headerStyle = headerElement - ? getComputedStyle(headerElement) - : getComputedStyle(tableElement); - const cellStyle = cellElement - ? getComputedStyle(cellElement) - : getComputedStyle(tableElement); - - const headerFont = headerStyle.font || "12px sans-serif"; - const cellFont = cellStyle.font || "12px sans-serif"; - - const headerExtraWidth = measureHeaderExtraWidth(tableElement); - const cellPadding = measureCellPadding(cellElement); - - const sampleSize = Math.min(50, data.length); - - for (const column of columns) { - const id = getColumnId(column); - const accessorKey = (column as { accessorKey?: string }).accessorKey; - if (!id || !accessorKey) continue; - - const headerText = String(column.header || ""); - const headerTextWidth = measureTextWidth( - headerText, - headerFont, - measureContainer - ); - const headerWidth = headerTextWidth + headerExtraWidth; - - // Use column's textValue function if available, otherwise fall back to String() - // If textValue returns null, skip content measurement for this column - const getTextValue = - column.textValue ?? ((v: unknown) => (v == null ? "-" : String(v))); - - let maxContentWidth = 0; - let skipContentMeasurement = false; - - for (let i = 0; i < sampleSize; i++) { - const row = data[i]; - if (!row) continue; - const value = (row as Record)[accessorKey]; - const textContent = getTextValue(value); - - // If textValue returns null, skip content measurement entirely - if (textContent === null) { - skipContentMeasurement = true; - break; - } - - const contentWidth = measureTextWidth( - textContent, - cellFont, - measureContainer - ); - maxContentWidth = Math.max(maxContentWidth, contentWidth); - } - - // If skipping content measurement, only use header width - if (skipContentMeasurement) { - maxContentWidth = 0; - } - - const contentWidthWithPadding = maxContentWidth + cellPadding; - const idealSize = Math.max(headerWidth, contentWidthWithPadding); - - const constraint = constraints.get(id); - if (constraint) { - sizing[id] = clampSize(idealSize, constraint); - } else { - sizing[id] = idealSize; - } - } - } finally { - document.body.removeChild(measureContainer); - } - - return sizing; - }, -}; diff --git a/src/inspect_scout/_view/www/src/app/components/columnSizing/index.ts b/src/inspect_scout/_view/www/src/app/components/columnSizing/index.ts deleted file mode 100644 index 3d82fe136..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnSizing/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { - clampSize, - getColumnConstraints, - getColumnId, - DEFAULT_MAX_SIZE, - DEFAULT_MIN_SIZE, - DEFAULT_SIZE, -} from "./types"; - -export type { - ColumnSizeConstraints, - ColumnSizingStrategyKey, - SizingStrategy, - SizingStrategyContext, -} from "./types"; - -export { defaultStrategy } from "./defaultStrategy"; -export { fitContentStrategy } from "./fitContentStrategy"; -export { getSizingStrategy, sizingStrategies } from "./strategies"; diff --git a/src/inspect_scout/_view/www/src/app/components/columnSizing/strategies.ts b/src/inspect_scout/_view/www/src/app/components/columnSizing/strategies.ts deleted file mode 100644 index f140d0025..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnSizing/strategies.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Column sizing strategy registry. - */ - -import { defaultStrategy } from "./defaultStrategy"; -import { fitContentStrategy } from "./fitContentStrategy"; -import { ColumnSizingStrategyKey, SizingStrategy } from "./types"; - -/** - * Registry of all available sizing strategies. - */ -export const sizingStrategies: Record = - { - default: defaultStrategy, - "fit-content": fitContentStrategy, - }; - -/** - * Get a sizing strategy by key. - * Falls back to default strategy if key is not found. - */ -export function getSizingStrategy( - key: ColumnSizingStrategyKey -): SizingStrategy { - return sizingStrategies[key] ?? sizingStrategies.default; -} diff --git a/src/inspect_scout/_view/www/src/app/components/columnSizing/types.ts b/src/inspect_scout/_view/www/src/app/components/columnSizing/types.ts deleted file mode 100644 index 953dca176..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnSizing/types.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Generic column sizing types for data grids. - * These types are used by the column sizing strategies and hooks. - */ - -import { ColumnSizingState } from "@tanstack/react-table"; - -import { ExtendedColumnDef, BaseColumnMeta } from "../columnTypes"; - -/** - * Size constraints for a column. - */ -export interface ColumnSizeConstraints { - /** Default size in pixels */ - size: number; - /** Minimum allowed size in pixels */ - minSize: number; - /** Maximum allowed size in pixels */ - maxSize: number; -} - -/** Default minimum column size in pixels */ -export const DEFAULT_MIN_SIZE = 40; - -/** Default maximum column size in pixels */ -export const DEFAULT_MAX_SIZE = 600; - -/** Default column size in pixels when not specified */ -export const DEFAULT_SIZE = 150; - -/** - * Context provided to sizing strategies for computing column sizes. - * Generic over TData (row data type). - */ -export interface SizingStrategyContext { - /** The table element for DOM measurements (may be null) */ - tableElement: HTMLTableElement | null; - /** Column definitions */ - columns: ExtendedColumnDef[]; - /** Current data for content measurement */ - data: TData[]; - /** Pre-computed constraints for each column */ - constraints: Map; -} - -/** - * Interface for column sizing strategies. - * Each strategy computes column sizes differently. - * Generic over TData (row data type). - */ -export interface SizingStrategy { - /** Compute sizes for all columns */ - computeSizes(context: SizingStrategyContext): ColumnSizingState; -} - -/** - * Available sizing strategy keys. - * - "default": Uses the column's defined `size` property - * - "fit-content": Measures content and sizes columns to fit within min/max constraints - */ -export type ColumnSizingStrategyKey = "default" | "fit-content"; - -/** - * Clamp a size value to min/max constraints. - */ -export function clampSize( - size: number, - constraints: ColumnSizeConstraints -): number { - return Math.max(constraints.minSize, Math.min(constraints.maxSize, size)); -} - -/** - * Get the column ID from a column definition. - */ -export function getColumnId( - column: ExtendedColumnDef -): string { - return column.id || (column as { accessorKey?: string }).accessorKey || ""; -} - -/** - * Extract size constraints from column definitions. - */ -export function getColumnConstraints( - columns: ExtendedColumnDef[] -): Map { - const constraints = new Map(); - - for (const column of columns) { - const id = getColumnId(column); - if (id) { - constraints.set(id, { - size: column.size ?? DEFAULT_SIZE, - minSize: column.minSize ?? DEFAULT_MIN_SIZE, - maxSize: column.maxSize ?? DEFAULT_MAX_SIZE, - }); - } - } - - return constraints; -} diff --git a/src/inspect_scout/_view/www/src/app/components/columnTypes.ts b/src/inspect_scout/_view/www/src/app/components/columnTypes.ts deleted file mode 100644 index be5e0b169..000000000 --- a/src/inspect_scout/_view/www/src/app/components/columnTypes.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ColumnDef } from "@tanstack/react-table"; - -import type { FilterType } from "../../state/store"; - -/** - * Common column metadata properties shared across all data grids. - */ -export interface BaseColumnMeta { - /** Text alignment for the column */ - align?: "left" | "center" | "right"; - /** Whether the column can be filtered */ - filterable?: boolean; - /** Filter type for the column */ - filterType?: FilterType; -} - -/** - * Extended column definition with custom properties for data grids. - * Extends TanStack Table's ColumnDef with additional rendering helpers. - */ -export type ExtendedColumnDef< - TData, - TMeta extends BaseColumnMeta = BaseColumnMeta, -> = ColumnDef & { - meta?: TMeta; - /** Returns string for tooltip display */ - titleValue?: (value: unknown) => string; - /** Returns string representation for column width measurement. Return null to skip content measurement. */ - textValue?: (value: unknown) => string | null; - /** Minimum column width in pixels */ - minSize?: number; - /** Maximum column width in pixels */ - maxSize?: number; - /** Tooltip text for the column header */ - headerTitle?: string; -}; - -/** - * Configuration for the column picker component. - */ -export interface ColumnInfo { - /** Column identifier */ - id: string; - /** Display label for the column */ - label: string; - /** Optional tooltip for the column header */ - headerTitle?: string; -} - -/** - * Extract title value for tooltip from a cell. - * Uses custom titleValue function if provided, otherwise falls back to default formatting. - */ -export function getCellTitleValue( - value: unknown, - columnDef: ExtendedColumnDef -): string { - // Use custom titleValue function if provided - if (columnDef.titleValue) { - return columnDef.titleValue(value); - } - - // Default fallback - if (value === undefined || value === null) { - return ""; - } - if (typeof value === "object") { - return JSON.stringify(value, null, 2); - } - return String(value); -} diff --git a/src/inspect_scout/_view/www/src/app/components/dataGrid/DataGrid.module.css b/src/inspect_scout/_view/www/src/app/components/dataGrid/DataGrid.module.css deleted file mode 100644 index 89959d099..000000000 --- a/src/inspect_scout/_view/www/src/app/components/dataGrid/DataGrid.module.css +++ /dev/null @@ -1,172 +0,0 @@ -.container { - overflow: auto; - outline: none; -} - -.table { - display: grid; - width: 100%; -} - -.thead { - display: grid; - position: sticky; - top: 0; - z-index: 1; - background: var(--bs-light-bg-subtle); - border-bottom: 1px solid var(--bs-border-color); -} - -.headerRow { - display: flex; - width: 100%; - height: 32px; -} - -.headerCell { - display: flex; - position: relative; - align-items: center; - justify-content: space-between; - gap: 4px; - padding: 4px 0px 4px 8px; - text-align: left; - font-weight: 400; - color: var(--bs-secondary); - font-size: var(--inspect-font-size-small) !important; - transition: background-color 0.15s ease; -} - -.headerContent { - flex: 1; - display: flex; - align-items: center; - gap: 4px; - cursor: grab; - user-select: none; -} - -.headerContent:active { - cursor: grabbing; -} - -.headerText { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.sortIcon { - font-size: 10px; - margin-left: 2px; -} - -.headerCellDragging { - opacity: 1; - background: var(--bs-light-bg-subtle); -} - -.headerCellDragOverLeft { - border-left: 3px solid var(--bs-primary); -} - -.headerCellDragOverRight { - border-right: 3px solid var(--bs-primary); -} - -.resizer { - position: absolute; - right: 0; - top: 0; - margin-top: 4px; - margin-bottom: 4px; - height: calc(100% - 8px); - width: 5px; - background: transparent; - cursor: col-resize; - user-select: none; - touch-action: none; - border-right: solid 1px var(--bs-border-color); - z-index: 10; - pointer-events: auto; -} - -.headerCellDragging .resizer { - border-right: none; -} - -.resizer:hover { - background: var(--bs-primary); - opacity: 0.5; -} - -.resizerActive { - background: var(--bs-primary); - opacity: 1; -} - -.headerCell:last-child { - border-right: none; -} - -.tbody { - display: grid; - position: relative; -} - -.row { - display: flex; - position: absolute; - width: 100%; - border-bottom: 1px solid var(--bs-border-color); - background: var(--bs-body-bg); - transition: background-color 0.15s ease; - cursor: pointer; - user-select: none; -} - -.row:hover { - background: var(--bs-light); -} - -.rowSelected { - background: var(--bs-primary-bg-subtle) !important; -} - -.rowFocused { - outline: 2px solid var(--bs-primary-bg-subtle); - outline-offset: 0px; -} - -.rowSelected.rowFocused { - background: var(--bs-primary-bg-subtle) !important; -} - -.cell { - display: flex; - align-items: center; - padding: 4px 8px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 12px !important; -} - -.cell:last-child { - border-right: none; -} - -.cellCenter { - justify-content: center; -} - -.headerCellCenter { - justify-content: center; -} - -.noMatching { - grid-column: 1 / -1; - margin-left: auto; - margin-right: auto; - margin-top: 3rem; -} diff --git a/src/inspect_scout/_view/www/src/app/components/dataGrid/DataGrid.tsx b/src/inspect_scout/_view/www/src/app/components/dataGrid/DataGrid.tsx deleted file mode 100644 index 25f6fe9df..000000000 --- a/src/inspect_scout/_view/www/src/app/components/dataGrid/DataGrid.tsx +++ /dev/null @@ -1,777 +0,0 @@ -import { - flexRender, - getCoreRowModel, - OnChangeFn, - RowSelectionState, - SortingState, - ColumnSizingState, - useReactTable, -} from "@tanstack/react-table"; -import { useVirtualizer } from "@tanstack/react-virtual"; -import clsx from "clsx"; -import { - DragEvent, - KeyboardEvent, - MouseEvent, - ReactElement, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; - -import { ApplicationIcons } from "../../../components/icons"; -import { useLoggingNavigate } from "../../../debugging/navigationDebugging"; -import type { SimpleCondition } from "../../../query/types"; -import { openRouteInNewTab } from "../../../router/url"; -import { FilterType } from "../../../state/store"; -import { ColumnFilterControl } from "../columnFilter"; -import { - getCellTitleValue, - ExtendedColumnDef, - BaseColumnMeta, -} from "../columnTypes"; - -import styles from "./DataGrid.module.css"; -import type { DataGridProps, DataGridTableState } from "./types"; - -/** - * Shared DataGrid component with virtual scrolling, sorting, filtering, - * row selection, keyboard navigation, and column reordering. - */ -export function DataGrid< - TData, - TColumn extends ExtendedColumnDef, - TState extends DataGridTableState = DataGridTableState, ->({ - // Data - data, - columns, - getRowId, - getRowKey, - - // State (consolidated) - state, - - // State setter - onStateChange, - - // Navigation - getRowRoute, - - // Infinite scroll - onScrollNearEnd, - hasMore = false, - fetchThreshold = 500, - - // Filtering - filterSuggestions = [], - onFilterColumnChange, - - // Column sizing - onColumnSizingChange, - onResetColumnSize, - - // UI - className, - loading = false, - emptyMessage = "No matching items", - noConfigMessage = "No directory configured.", -}: DataGridProps): ReactElement { - // Destructure state for convenience - const { - sorting, - columnOrder, - columnFilters, - columnSizing, - rowSelection, - focusedRowId, - } = state; - - // Refs - const containerRef = useRef(null); - const tableRef = useRef(null); - const navigate = useLoggingNavigate("DataGrid"); - - // Default getRowKey to getRowId if not provided - const effectiveGetRowKey = useCallback( - (index: number, row?: TData): string => { - if (getRowKey) { - return getRowKey(index, row); - } - if (row) { - return getRowId(row); - } - return String(index); - }, - [getRowKey, getRowId] - ); - - // Column filter change handler - const handleColumnFilterChange = useCallback( - ( - columnId: string, - filterType: FilterType, - condition: SimpleCondition | null - ) => { - onStateChange((prev) => { - if (condition === null) { - // Remove the filter entirely - const newFilters = { ...prev.columnFilters }; - delete newFilters[columnId]; - return { - ...prev, - columnFilters: newFilters, - }; - } - // Add or update the filter - return { - ...prev, - columnFilters: { - ...prev.columnFilters, - [columnId]: { - columnId, - filterType, - condition, - }, - }, - }; - }); - }, - [onStateChange] - ); - - // Column ordering handler - const handleColumnOrderChange: OnChangeFn = useCallback( - (updaterOrValue) => { - onStateChange((prev) => { - const newValue = - typeof updaterOrValue === "function" - ? updaterOrValue(prev.columnOrder) - : updaterOrValue; - return { ...prev, columnOrder: newValue }; - }); - }, - [onStateChange] - ); - - // Sorting handler - const handleSortingChange: OnChangeFn = useCallback( - (updaterOrValue) => { - onStateChange((prev) => { - const newValue = - typeof updaterOrValue === "function" - ? updaterOrValue(prev.sorting) - : updaterOrValue; - return { ...prev, sorting: newValue }; - }); - }, - [onStateChange] - ); - - // Row selection handler - const handleRowSelectionChange: OnChangeFn = useCallback( - (updaterOrValue) => { - onStateChange((prev) => { - const newValue = - typeof updaterOrValue === "function" - ? updaterOrValue(prev.rowSelection) - : updaterOrValue; - return { ...prev, rowSelection: newValue }; - }); - }, - [onStateChange] - ); - - // Column sizing handler - const handleColumnSizingChange: OnChangeFn = useCallback( - (updaterOrValue) => { - onStateChange((prev) => { - const newValue = - typeof updaterOrValue === "function" - ? updaterOrValue(prev.columnSizing) - : updaterOrValue; - return { ...prev, columnSizing: newValue }; - }); - - // Also call external handler if provided - if (onColumnSizingChange) { - const newValue = - typeof updaterOrValue === "function" - ? updaterOrValue(columnSizing) - : updaterOrValue; - onColumnSizingChange(newValue); - } - }, - [onStateChange, onColumnSizingChange, columnSizing] - ); - - // Compute effective column order - const effectiveColumnOrder = useMemo(() => { - if (columnOrder && columnOrder.length > 0) { - return columnOrder; - } - // Default to column order from column definitions - return columns.map( - (col) => - (col.id ?? (col as { accessorKey?: string }).accessorKey) as string - ); - }, [columnOrder, columns]); - - // Drag and drop state - const [draggedColumn, setDraggedColumn] = useState(null); - const [dragOverColumn, setDragOverColumn] = useState(null); - const [dropPosition, setDropPosition] = useState<"left" | "right" | null>( - null - ); - - const resetDragState = useCallback(() => { - setDraggedColumn(null); - setDragOverColumn(null); - setDropPosition(null); - }, []); - - const handleDragStart = useCallback( - (e: DragEvent, columnId: string) => { - setDraggedColumn(columnId); - e.dataTransfer.effectAllowed = "move"; - }, - [] - ); - - const handleDragOver = useCallback( - (e: DragEvent, columnId: string) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - - // Only set drag over if it's a different column than the one being dragged - if (draggedColumn && draggedColumn !== columnId) { - setDragOverColumn(columnId); - - // Determine which side to show the drop indicator - const draggedIndex = effectiveColumnOrder.indexOf(draggedColumn); - const targetIndex = effectiveColumnOrder.indexOf(columnId); - - // If dragging from left to right, show indicator on right side - setDropPosition(draggedIndex < targetIndex ? "right" : "left"); - } - }, - [draggedColumn, effectiveColumnOrder] - ); - - const handleDragLeave = useCallback(() => { - setDragOverColumn(null); - setDropPosition(null); - }, []); - - const handleDrop = useCallback( - (e: DragEvent, targetColumnId: string) => { - e.preventDefault(); - - if (!draggedColumn || draggedColumn === targetColumnId) { - resetDragState(); - return; - } - - const draggedIndex = effectiveColumnOrder.indexOf(draggedColumn); - const targetIndex = effectiveColumnOrder.indexOf(targetColumnId); - - if (draggedIndex === -1 || targetIndex === -1) { - resetDragState(); - return; - } - - // Create new order by moving dragged column to target position - const newOrder = [...effectiveColumnOrder]; - newOrder.splice(draggedIndex, 1); - newOrder.splice(targetIndex, 0, draggedColumn); - - onStateChange((prev) => ({ ...prev, columnOrder: newOrder })); - resetDragState(); - }, - [draggedColumn, effectiveColumnOrder, onStateChange, resetDragState] - ); - - const handleDragEnd = useCallback(() => { - resetDragState(); - }, [resetDragState]); - - // Create table instance - // useReactTable returns unmemoizable functions - // https://github.com/TanStack/table/issues/5567 - // https://github.com/facebook/react/issues/33057 - // eslint-disable-next-line react-hooks/incompatible-library - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - manualSorting: true, - columnResizeMode: "onChange", - enableColumnResizing: true, - enableSorting: true, - enableMultiSort: true, - enableSortingRemoval: true, - maxMultiSortColCount: 3, - enableRowSelection: true, - getRowId, - state: { - columnSizing, - columnOrder: effectiveColumnOrder, - sorting, - rowSelection, - }, - onColumnSizingChange: handleColumnSizingChange, - onColumnOrderChange: handleColumnOrderChange, - onSortingChange: handleSortingChange, - onRowSelectionChange: handleRowSelectionChange, - }); - - const { rows } = table.getRowModel(); - - // Row click handler with selection support - const handleRowClick = useCallback( - (e: MouseEvent, rowId: string, rowIndex: number) => { - // Focus the container to enable keyboard navigation - if (containerRef.current) { - containerRef.current.focus(); - } - - // Update focused row - onStateChange((prev) => ({ ...prev, focusedRowId: rowId })); - - const row = rows[rowIndex]; - if (!row) return; - - if (e.metaKey || e.ctrlKey) { - // Cmd/Ctrl + Click: Open in new tab - openRouteInNewTab(getRowRoute(row.original)); - } else if (e.shiftKey) { - // Shift + Click: Range selection - const currentSelectedRows = Object.keys(rowSelection).filter( - (id) => rowSelection[id] - ); - if (currentSelectedRows.length > 0) { - // Find the last selected row - const lastSelectedId = - currentSelectedRows[currentSelectedRows.length - 1]; - const lastSelectedIndex = rows.findIndex( - (r) => r.id === lastSelectedId - ); - - if (lastSelectedIndex !== -1) { - const start = Math.min(lastSelectedIndex, rowIndex); - const end = Math.max(lastSelectedIndex, rowIndex); - const newSelection: RowSelectionState = {}; - - for (let i = start; i <= end; i++) { - const r = rows[i]; - if (r) { - newSelection[r.id] = true; - } - } - - onStateChange((prev) => ({ - ...prev, - rowSelection: newSelection, - })); - } - } else { - // No previous selection, just select this row - onStateChange((prev) => ({ - ...prev, - rowSelection: { [rowId]: true }, - })); - } - } else { - // Normal click: Navigate to row - void navigate(getRowRoute(row.original)); - } - }, - [rows, rowSelection, onStateChange, navigate, getRowRoute] - ); - - // Keyboard navigation handler - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (rows.length === 0) return; - - // Find the currently focused row index - const focusedIndex = focusedRowId - ? rows.findIndex((r) => r.id === focusedRowId) - : -1; - - let newFocusedIndex = focusedIndex; - let shouldUpdateSelection = false; - let shouldExtendSelection = false; - - switch (e.key) { - case "ArrowDown": - e.preventDefault(); - if (e.metaKey || e.ctrlKey) { - // Cmd/Ctrl + Arrow Down: Jump to bottom - newFocusedIndex = rows.length - 1; - } else { - newFocusedIndex = Math.min(focusedIndex + 1, rows.length - 1); - } - shouldUpdateSelection = !e.shiftKey; - shouldExtendSelection = e.shiftKey; - break; - - case "ArrowUp": - e.preventDefault(); - if (e.metaKey || e.ctrlKey) { - // Cmd/Ctrl + Arrow Up: Jump to top - newFocusedIndex = 0; - } else { - newFocusedIndex = Math.max(focusedIndex - 1, 0); - } - shouldUpdateSelection = !e.shiftKey; - shouldExtendSelection = e.shiftKey; - break; - - case "Enter": - e.preventDefault(); - if (focusedIndex !== -1) { - const row = rows[focusedIndex]; - if (row) { - void navigate(getRowRoute(row.original)); - } - } - return; - - case " ": - e.preventDefault(); - if (focusedIndex !== -1) { - const row = rows[focusedIndex]; - if (row) { - onStateChange((prev) => ({ - ...prev, - rowSelection: { - ...prev.rowSelection, - [row.id]: !prev.rowSelection[row.id], - }, - })); - } - } - return; - - case "a": - if (e.metaKey || e.ctrlKey) { - e.preventDefault(); - // Select all - const allSelected: RowSelectionState = {}; - rows.forEach((row) => { - allSelected[row.id] = true; - }); - onStateChange((prev) => ({ - ...prev, - rowSelection: allSelected, - })); - } - return; - - case "Escape": - e.preventDefault(); - // Clear selection - onStateChange((prev) => ({ - ...prev, - rowSelection: {}, - })); - return; - - default: - return; - } - - // Handle arrow key navigation - if (newFocusedIndex !== focusedIndex && newFocusedIndex !== -1) { - const newRow = rows[newFocusedIndex]; - if (!newRow) return; - - onStateChange((prev) => ({ - ...prev, - focusedRowId: newRow.id, - })); - - if (shouldUpdateSelection) { - // Normal arrow: move selection - onStateChange((prev) => ({ - ...prev, - rowSelection: { [newRow.id]: true }, - })); - } else if (shouldExtendSelection) { - // Shift + arrow: extend selection - const currentSelectedRows = Object.keys(rowSelection).filter( - (id) => rowSelection[id] - ); - if (currentSelectedRows.length > 0) { - // Find the anchor (first selected row) - const anchorId = currentSelectedRows[0]; - const anchorIndex = rows.findIndex((r) => r.id === anchorId); - - if (anchorIndex !== -1) { - const start = Math.min(anchorIndex, newFocusedIndex); - const end = Math.max(anchorIndex, newFocusedIndex); - const newSelection: RowSelectionState = {}; - - for (let i = start; i <= end; i++) { - const r = rows[i]; - if (r) { - newSelection[r.id] = true; - } - } - - onStateChange((prev) => ({ - ...prev, - rowSelection: newSelection, - })); - } - } else { - // No selection, start new one - onStateChange((prev) => ({ - ...prev, - rowSelection: { [newRow.id]: true }, - })); - } - } - } - }, - [rows, focusedRowId, rowSelection, onStateChange, navigate, getRowRoute] - ); - - // Create virtualizer - const rowVirtualizer = useVirtualizer({ - count: rows.length, - getScrollElement: () => containerRef.current, - estimateSize: () => 29, - overscan: 10, - getItemKey: (index) => effectiveGetRowKey(index, rows[index]?.original), - }); - - const virtualItems = rowVirtualizer.getVirtualItems(); - const totalSize = rowVirtualizer.getTotalSize(); - - // Infinite scroll: notify parent when scrolled near bottom - const checkScrollNearEnd = useCallback( - (containerRefElement?: HTMLDivElement | null) => { - if (!containerRefElement || !hasMore || !onScrollNearEnd) return; - - const { scrollHeight, scrollTop, clientHeight } = containerRefElement; - const distanceFromBottom = scrollHeight - scrollTop - clientHeight; - - if (distanceFromBottom < fetchThreshold) { - onScrollNearEnd(distanceFromBottom); - } - }, - [onScrollNearEnd, hasMore, fetchThreshold] - ); - - // Check on mount if we need to fetch more - useEffect(() => { - checkScrollNearEnd(containerRef.current); - }, [checkScrollNearEnd]); - - // Scroll focused row into view when it changes - useEffect(() => { - if (focusedRowId && containerRef.current) { - const focusedIndex = rows.findIndex((r) => r.id === focusedRowId); - if (focusedIndex !== -1) { - // For the last item, scroll to the very bottom - if (focusedIndex === rows.length - 1) { - containerRef.current.scrollTop = containerRef.current.scrollHeight; - } else { - rowVirtualizer.scrollToIndex(focusedIndex, { - align: "center", - behavior: "auto", - }); - } - } - } - }, [focusedRowId, rows, rowVirtualizer]); - - const onScroll = useCallback( - (e: React.UIEvent) => checkScrollNearEnd(e.currentTarget), - [checkScrollNearEnd] - ); - - // Get empty state message - const getEmptyMessage = (): string => { - if (loading) return "Loading..."; - if (!data.length && noConfigMessage) return noConfigMessage; - return emptyMessage; - }; - - return ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const columnDef = header.column.columnDef as TColumn; - const columnMeta = columnDef.meta; - const align = columnMeta?.align; - const filterType = columnMeta?.filterType; - return ( - - ); - })} - - ))} - - - {virtualItems.length > 0 ? ( - virtualItems.map((virtualRow) => { - const row = rows[virtualRow.index]; - if (!row) return null; - - const isSelected = row.getIsSelected(); - const isFocused = focusedRowId === row.id; - const rowKey = effectiveGetRowKey(virtualRow.index, row.original); - - return ( - handleRowClick(e, row.id, virtualRow.index)} - > - {row.getVisibleCells().map((cell) => { - const cellColumnDef = cell.column.columnDef as TColumn; - const cellAlign = cellColumnDef.meta?.align; - const titleValue = getCellTitleValue( - cell.getValue(), - cellColumnDef - ); - return ( - - ); - })} - - ); - }) - ) : ( - - - - )} - -
handleDragOver(e, header.column.id)} - onDragLeave={handleDragLeave} - onDrop={(e) => handleDrop(e, header.column.id)} - title={[header.column.id, columnDef.headerTitle] - .filter(Boolean) - .join("\n")} - > -
handleDragStart(e, header.column.id)} - onDragEnd={handleDragEnd} - onClick={header.column.getToggleSortingHandler()} - style={{ - cursor: "pointer", - maxWidth: `calc(${header.getSize()}px - 32px)`, - }} - > - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - {{ - asc: ( - - ), - desc: ( - - ), - }[header.column.getIsSorted() as string] ?? null} -
- {columnMeta?.filterable && filterType ? ( - - handleColumnFilterChange( - header.column.id, - filterType, - condition - ) - } - suggestions={filterSuggestions} - onOpenChange={onFilterColumnChange} - /> - ) : null} -
- onResetColumnSize?.(header.column.id) - } - /> -
- {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} -
{getEmptyMessage()}
-
- ); -} diff --git a/src/inspect_scout/_view/www/src/app/components/dataGrid/index.ts b/src/inspect_scout/_view/www/src/app/components/dataGrid/index.ts deleted file mode 100644 index ec7074737..000000000 --- a/src/inspect_scout/_view/www/src/app/components/dataGrid/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { DataGrid } from "./DataGrid"; -export type { - DataGridProps, - DataGridTableState, - ColumnFilterChangeHandler, - ExtendedColumnDef, - BaseColumnMeta, -} from "./types"; diff --git a/src/inspect_scout/_view/www/src/app/components/dataGrid/types.ts b/src/inspect_scout/_view/www/src/app/components/dataGrid/types.ts deleted file mode 100644 index ed4eb7c27..000000000 --- a/src/inspect_scout/_view/www/src/app/components/dataGrid/types.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - ColumnSizingState, - RowSelectionState, - SortingState, -} from "@tanstack/react-table"; - -import type { ScalarValue } from "../../../api/api"; -import type { SimpleCondition } from "../../../query/types"; -import type { ColumnFilter, FilterType } from "../../../state/store"; -import type { ExtendedColumnDef, BaseColumnMeta } from "../columnTypes"; - -/** - * Props for the shared DataGrid component. - * Designed to work with any data type and column configuration. - * - * @template TData - The row data type - * @template TColumn - The column definition type - * @template TState - The table state type (must extend DataGridTableState) - */ -export interface DataGridProps< - TData, - TColumn extends ExtendedColumnDef, - TState extends DataGridTableState = DataGridTableState, -> { - // Data - /** Array of row data to display */ - data: TData[]; - /** Column definitions for the grid */ - columns: TColumn[]; - /** Get unique ID for a row */ - getRowId: (row: TData) => string; - /** Get stable key for virtualizer (defaults to getRowId) */ - getRowKey?: (index: number, row?: TData) => string; - - // State (consolidated into single object) - /** All table state managed by parent store */ - state: DataGridTableState; - - // State setter (single update function for all state) - // Uses generic TState to allow specific store types - onStateChange: (updater: TState | ((prev: TState) => TState)) => void; - - // Navigation - /** Get the route for a row (for navigation) */ - getRowRoute: (row: TData) => string; - - // Infinite scroll (optional) - /** Called when scroll position nears end. Receives distance from bottom in px. */ - onScrollNearEnd?: (distanceFromBottom: number) => void; - /** Whether more data is available to fetch */ - hasMore?: boolean; - /** Distance from bottom (in px) at which to trigger callback */ - fetchThreshold?: number; - - // Filtering (optional) - /** Autocomplete suggestions for the currently editing filter column */ - filterSuggestions?: ScalarValue[]; - /** Called when a filter column starts/stops being edited */ - onFilterColumnChange?: (columnId: string | null) => void; - - // Column sizing (optional) - /** Handler for column sizing changes */ - onColumnSizingChange?: (sizing: ColumnSizingState) => void; - /** Reset a single column to its auto-calculated size */ - onResetColumnSize?: (columnId: string) => void; - - // UI - /** Additional CSS class names */ - className?: string | string[]; - /** Whether data is loading */ - loading?: boolean; - /** Message to show when there's no data */ - emptyMessage?: string; - /** Message to show when there's no configuration (e.g., no directory) */ - noConfigMessage?: string; -} - -/** - * Table state managed by parent store. - * This is the shape of state that setTableState updater receives. - */ -export interface DataGridTableState { - columnSizing: ColumnSizingState; - columnOrder: string[]; - sorting: SortingState; - rowSelection: RowSelectionState; - focusedRowId: string | null; - columnFilters: Record; -} - -/** - * Handler for column filter changes. - */ -export type ColumnFilterChangeHandler = ( - columnId: string, - filterType: FilterType, - condition: SimpleCondition | null -) => void; - -/** - * Re-export column types for convenience - */ -export type { ExtendedColumnDef, BaseColumnMeta }; diff --git a/src/inspect_scout/_view/www/src/app/components/useBreadcrumbTruncation.ts b/src/inspect_scout/_view/www/src/app/components/useBreadcrumbTruncation.ts deleted file mode 100644 index f14b545e0..000000000 --- a/src/inspect_scout/_view/www/src/app/components/useBreadcrumbTruncation.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { useEffect, useState, RefObject } from "react"; - -export interface BreadcrumbSegment { - text: string; - url?: string; -} - -interface TruncatedBreadcrumbs { - visibleSegments: BreadcrumbSegment[]; - hiddenCount: number; - showEllipsis: boolean; -} - -export const useBreadcrumbTruncation = ( - segments: BreadcrumbSegment[], - containerRef: RefObject -): TruncatedBreadcrumbs => { - const [truncatedData, setTruncatedData] = useState({ - visibleSegments: segments, - hiddenCount: 0, - showEllipsis: false, - }); - - useEffect(() => { - const measureAndTruncate = () => { - if (!containerRef.current || segments.length <= 3) { - setTruncatedData({ - visibleSegments: segments, - hiddenCount: 0, - showEllipsis: false, - }); - return; - } - - const container = containerRef.current; - const containerWidth = container.offsetWidth; - - // Create a test element to measure breadcrumb widths - const testElement = document.createElement("ol"); - testElement.className = "breadcrumb"; - testElement.style.position = "absolute"; - testElement.style.visibility = "hidden"; - testElement.style.whiteSpace = "nowrap"; - testElement.style.margin = "0"; - testElement.style.padding = "0"; - - container.appendChild(testElement); - - // Test if all segments fit - testElement.innerHTML = segments - .map((segment) => ``) - .join(""); - - if (testElement.scrollWidth <= containerWidth) { - container.removeChild(testElement); - setTruncatedData({ - visibleSegments: segments, - hiddenCount: 0, - showEllipsis: false, - }); - return; - } - - // Find the maximum number of segments we can show - // Always keep first and last segments - const firstSegment = segments[0]; - const lastSegment = segments[segments.length - 1]; - - if (!firstSegment || !lastSegment) { - container.removeChild(testElement); - return; - } - - let maxVisible = 2; // Start with just first and last - - // Try adding segments from the end (most recent path) first - for (let endCount = 1; endCount < segments.length - 1; endCount++) { - const candidateSegments = [ - firstSegment, - ...segments.slice(segments.length - 1 - endCount, -1), - lastSegment, - ]; - - // Test with ellipsis - const testHTML = [ - ``, - ``, - ...segments - .slice(segments.length - 1 - endCount, -1) - .map((s) => ``), - ``, - ].join(""); - - testElement.innerHTML = testHTML; - - if (testElement.scrollWidth <= containerWidth) { - maxVisible = candidateSegments.length; - setTruncatedData({ - visibleSegments: candidateSegments, - hiddenCount: segments.length - candidateSegments.length, - showEllipsis: true, - }); - } else { - break; - } - } - - // If we couldn't fit any middle segments, just show first ... last - if (maxVisible === 2) { - setTruncatedData({ - visibleSegments: [firstSegment, lastSegment], - hiddenCount: segments.length - 2, - showEllipsis: true, - }); - } - - container.removeChild(testElement); - }; - - measureAndTruncate(); - - const resizeObserver = new ResizeObserver(measureAndTruncate); - if (containerRef.current) { - resizeObserver.observe(containerRef.current); - } - - return () => { - resizeObserver.disconnect(); - }; - }, [segments, containerRef]); - - return truncatedData; -}; diff --git a/src/inspect_scout/_view/www/src/app/components/useFilterBarHandlers.ts b/src/inspect_scout/_view/www/src/app/components/useFilterBarHandlers.ts deleted file mode 100644 index 135487170..000000000 --- a/src/inspect_scout/_view/www/src/app/components/useFilterBarHandlers.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { useCallback, useMemo } from "react"; - -import type { SimpleCondition } from "../../query/types"; -import { ColumnFilter } from "../../state/store"; - -/** - * Base table state interface that filter bar handlers can work with. - * Both ScansTableState and TranscriptsTableState conform to this. - */ -interface BaseTableState { - columnFilters: Record; - visibleColumns?: string[]; - columnOrder: string[]; -} - -interface FilterBarHandlers { - /** - * Update a filter's condition or remove it if condition is null - */ - handleFilterChange: ( - columnId: string, - condition: SimpleCondition | null - ) => void; - /** - * Remove a filter by column ID - */ - removeFilter: (column: string) => void; - /** - * Add a new filter, ensuring the column is visible - */ - handleAddFilter: (filter: ColumnFilter) => void; -} - -/** - * Creates filter bar handler functions for a table state. - * This is the core logic shared between scans and transcripts filter bars. - * - * @param setTableState - Store setter function - * @param defaultVisibleColumns - Default columns when state doesn't have them - * @returns Object with handler functions - */ -function createFilterBarHandlers< - TColumnKey extends string, - TState extends BaseTableState = BaseTableState, ->( - setTableState: (updater: TState | ((prev: TState) => TState)) => void, - defaultVisibleColumns: readonly TColumnKey[] -): FilterBarHandlers { - const handleFilterChange = ( - columnId: string, - condition: SimpleCondition | null - ) => { - setTableState((prevState) => { - const newFilters = { ...prevState.columnFilters }; - if (condition === null) { - delete newFilters[columnId]; - } else { - const existingFilter = newFilters[columnId]; - if (existingFilter) { - newFilters[columnId] = { - ...existingFilter, - condition, - }; - } - } - return { - ...prevState, - columnFilters: newFilters, - }; - }); - }; - - const removeFilter = (column: string) => { - setTableState((prevState) => { - const newFilters = { ...prevState.columnFilters }; - delete newFilters[column]; - return { - ...prevState, - columnFilters: newFilters, - }; - }); - }; - - const handleAddFilter = (filter: ColumnFilter) => { - setTableState((prevState) => { - const columnKey = filter.columnId as TColumnKey; - - // Use default visible columns if not set in state - const currentVisibleColumns = - (prevState.visibleColumns as TColumnKey[] | undefined) ?? - ([...defaultVisibleColumns] as TColumnKey[]); - - // Check if we need to add this column to visible columns - const needsColumnVisible = !currentVisibleColumns.includes(columnKey); - - // Check if we need to add this column to column order - const columnOrder = prevState.columnOrder as TColumnKey[]; - const needsColumnOrder = - columnOrder.length > 0 && !columnOrder.includes(columnKey); - - return { - ...prevState, - columnFilters: { - ...prevState.columnFilters, - [filter.columnId]: filter, - }, - // Add the column to visible columns if it's not already there - ...(needsColumnVisible && { - visibleColumns: [...currentVisibleColumns, columnKey], - }), - // Add the column to column order if it's not already there - ...(needsColumnOrder && { - columnOrder: [...columnOrder, columnKey], - }), - }; - }); - }; - - return { - handleFilterChange, - removeFilter, - handleAddFilter, - }; -} - -interface UseFilterBarHandlersOptions< - TColumnKey extends string, - TState extends BaseTableState = BaseTableState, -> { - /** - * Store setter function that accepts an updater - */ - setTableState: (updater: TState | ((prev: TState) => TState)) => void; - /** - * Default visible columns to use when state doesn't have them set - */ - defaultVisibleColumns: readonly TColumnKey[]; -} - -/** - * Hook that provides common filter bar handlers for both scans and transcripts tables. - * Extracts the duplicated logic from ScansFilterBar and TranscriptFilterBar. - */ -export function useFilterBarHandlers< - TColumnKey extends string, - TState extends BaseTableState = BaseTableState, ->({ - setTableState, - defaultVisibleColumns, -}: UseFilterBarHandlersOptions): FilterBarHandlers { - // Memoize the handlers to maintain referential stability - const handlers = useMemo( - () => createFilterBarHandlers(setTableState, defaultVisibleColumns), - [setTableState, defaultVisibleColumns] - ); - - // Wrap in useCallback for consistent return types - const handleFilterChange = useCallback( - (columnId: string, condition: SimpleCondition | null) => { - handlers.handleFilterChange(columnId, condition); - }, - [handlers] - ); - - const removeFilter = useCallback( - (column: string) => { - handlers.removeFilter(column); - }, - [handlers] - ); - - const handleAddFilter = useCallback( - (filter: ColumnFilter) => { - handlers.handleAddFilter(filter); - }, - [handlers] - ); - - return { - handleFilterChange, - removeFilter, - handleAddFilter, - }; -} diff --git a/src/inspect_scout/_view/www/src/app/hooks/useFilterConditions.ts b/src/inspect_scout/_view/www/src/app/hooks/useFilterConditions.ts deleted file mode 100644 index e8f24168a..000000000 --- a/src/inspect_scout/_view/www/src/app/hooks/useFilterConditions.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Condition } from "../../query"; -import { SimpleCondition } from "../../query/types"; -import { useStore } from "../../state/store"; - -/** - * Build a combined filter condition from column filters. - * @param excludeColumnId - Optional column ID to exclude from the condition - */ -export const useFilterConditions = (excludeColumnId?: string) => { - // The applied filters - const columnFilters = - useStore((state) => state.transcriptsTableState.columnFilters) ?? {}; - - // Get conditions, optionally excluding a specific column - const filterConditions = Object.values(columnFilters) - .filter((filter) => !excludeColumnId || filter.columnId !== excludeColumnId) - .map((filter) => filter.condition) - .filter((condition): condition is SimpleCondition => Boolean(condition)); - - // Reduce to a single condition using 'and' - const condition = filterConditions.reduce( - (acc, condition) => (acc ? acc.and(condition) : condition), - undefined - ); - return condition; -}; diff --git a/src/inspect_scout/_view/www/src/app/hooks/useScanFilterConditions.ts b/src/inspect_scout/_view/www/src/app/hooks/useScanFilterConditions.ts deleted file mode 100644 index 04098b31e..000000000 --- a/src/inspect_scout/_view/www/src/app/hooks/useScanFilterConditions.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Condition } from "../../query"; -import { SimpleCondition } from "../../query/types"; -import { useStore } from "../../state/store"; - -/** - * Build a combined filter condition from scans column filters. - * @param excludeColumnId - Optional column ID to exclude from the condition - */ -export const useScanFilterConditions = (excludeColumnId?: string) => { - // The applied filters - const columnFilters = - useStore((state) => state.scansTableState.columnFilters) ?? {}; - - // Get conditions, optionally excluding a specific column - const filterConditions = Object.values(columnFilters) - .filter((filter) => !excludeColumnId || filter.columnId !== excludeColumnId) - .map((filter) => filter.condition) - .filter((condition): condition is SimpleCondition => Boolean(condition)); - - // Reduce to a single condition using 'and' - const condition = filterConditions.reduce( - (acc, condition) => (acc ? acc.and(condition) : condition), - undefined - ); - return condition; -}; diff --git a/src/inspect_scout/_view/www/src/app/hooks/useScanResultSummaries.ts b/src/inspect_scout/_view/www/src/app/hooks/useScanResultSummaries.ts deleted file mode 100644 index b186d6db3..000000000 --- a/src/inspect_scout/_view/www/src/app/hooks/useScanResultSummaries.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ColumnTable } from "arquero"; -import { useState, useMemo, useEffect } from "react"; - -import { ScanResultSummary } from "../types"; -import { parseScanResultSummaries } from "../utils/arrowHelpers"; - -export const useScanResultSummaries = (columnTable?: ColumnTable) => { - const [scanResultSummaries, setScanResultsSummaries] = useState< - ScanResultSummary[] - >([]); - const [isLoading, setIsLoading] = useState(false); - - const rowData = useMemo(() => columnTable?.objects(), [columnTable]); - - useEffect(() => { - if (!rowData || rowData.length === 0) { - // TODO: lint react-hooks/set-state-in-effect - consider if fixing this violation makes sense - /* eslint-disable react-hooks/set-state-in-effect */ - setScanResultsSummaries([]); - setIsLoading(false); - /* eslint-enable react-hooks/set-state-in-effect */ - return; - } - - let cancelled = false; - setIsLoading(true); - - const run = async () => { - try { - const result = await parseScanResultSummaries(rowData); - if (!cancelled) { - setScanResultsSummaries(result); - setIsLoading(false); - } - } catch (error) { - if (!cancelled) { - console.error("Error parsing scanner previews:", error); - setScanResultsSummaries([]); - setIsLoading(false); - } - } - }; - - void run(); - - return () => { - cancelled = true; - }; - }, [rowData]); - - return { data: scanResultSummaries, isLoading }; -}; diff --git a/src/inspect_scout/_view/www/src/app/hooks/useScanRoute.ts b/src/inspect_scout/_view/www/src/app/hooks/useScanRoute.ts deleted file mode 100644 index d6e75dec2..000000000 --- a/src/inspect_scout/_view/www/src/app/hooks/useScanRoute.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useEffect, useMemo } from "react"; -import { useParams } from "react-router-dom"; - -import { parseScanParams } from "../../router/url"; -import { useStore } from "../../state/store"; -import { join } from "../../utils/uri"; -import { useAppConfig } from "../server/useAppConfig"; - -export const useScanRoute = (): { - scansDir?: string; - relativePath: string; - scanPath: string; - scanResultUuid?: string; - resolvedScansDir?: string; - location?: string; -} => { - const params = useParams<{ scansDir?: string; "*": string }>(); - const setUserScansDir = useStore((state) => state.setUserScansDir); - const config = useAppConfig(); - const scansDir = config.scans.dir; - - const route = useMemo(() => parseScanParams(params), [params]); - const resolvedScansDir = route.scansDir || scansDir; - const location = resolvedScansDir - ? join(route.scanPath, resolvedScansDir) - : undefined; - - useEffect(() => { - if (route.scansDir) { - setUserScansDir(route.scansDir); - } - }, [route.scansDir, setUserScansDir]); - - return { - ...route, - resolvedScansDir, - location, - }; -}; diff --git a/src/inspect_scout/_view/www/src/app/hooks/useScansFilterBarProps.ts b/src/inspect_scout/_view/www/src/app/hooks/useScansFilterBarProps.ts deleted file mode 100644 index 6aa7fe701..000000000 --- a/src/inspect_scout/_view/www/src/app/hooks/useScansFilterBarProps.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; -import { useCallback, useState } from "react"; - -import { ScalarValue } from "../../api/api"; -import { useScansColumnValues } from "../server/useScansColumnValues"; - -import { useScanFilterConditions } from "./useScanFilterConditions"; - -interface ScansFilterBarProps { - /** Autocomplete suggestions for the currently edited column. */ - filterSuggestions: ScalarValue[]; - /** Callback when filter column selection changes. */ - onFilterColumnChange: (columnId: string | null) => void; -} - -/** - * Hook providing autocomplete props needed for ScansFilterBar. - * Manages filter editing state and fetches autocomplete suggestions. - */ -export const useScansFilterBarProps = ( - scansDir: string | undefined -): ScansFilterBarProps => { - const [editingColumnId, setEditingColumnId] = useState(null); - - // Get filter condition excluding the currently editing column - const otherColumnsFilter = useScanFilterConditions( - editingColumnId ?? undefined - ); - - // Fetch autocomplete suggestions for editing column - const { data: filterSuggestions } = useScansColumnValues( - editingColumnId && scansDir - ? { - location: scansDir, - column: editingColumnId, - filter: otherColumnsFilter, - } - : skipToken - ); - - const onFilterColumnChange = useCallback( - (columnId: string | null) => setEditingColumnId(columnId), - [] - ); - - return { - filterSuggestions: filterSuggestions ?? [], - onFilterColumnChange, - }; -}; diff --git a/src/inspect_scout/_view/www/src/app/hooks/useSelectedScan.ts b/src/inspect_scout/_view/www/src/app/hooks/useSelectedScan.ts deleted file mode 100644 index 8772c93b2..000000000 --- a/src/inspect_scout/_view/www/src/app/hooks/useSelectedScan.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; -import { useEffect } from "react"; - -import { useStore } from "../../state/store"; -import { Status } from "../../types/api-types"; -import { AsyncData } from "../../utils/asyncData"; -import { useScan } from "../server/useScan"; - -import { useScanRoute } from "./useScanRoute"; - -export const useSelectedScan = (): AsyncData => { - const { resolvedScansDir, scanPath } = useScanRoute(); - - // Set selectedScanLocation for nav restoration - const setSelectedScanLocation = useStore( - (state) => state.setSelectedScanLocation - ); - useEffect(() => { - if (scanPath) { - setSelectedScanLocation(scanPath); - } - }, [scanPath, setSelectedScanLocation]); - - return useScan( - resolvedScansDir && scanPath - ? { scansDir: resolvedScansDir, scanPath } - : skipToken - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanDataframe.ts b/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanDataframe.ts deleted file mode 100644 index 50182ed7f..000000000 --- a/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanDataframe.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; -import { ColumnTable } from "arquero"; - -import { AsyncData } from "../../utils/asyncData"; -import { useScanDataframe } from "../server/useScanDataframe"; - -import { useScanRoute } from "./useScanRoute"; -import { useSelectedScanner } from "./useSelectedScanner"; - -export const useSelectedScanDataframe = (): AsyncData => { - const { resolvedScansDir, scanPath } = useScanRoute(); - const scanner = useSelectedScanner(); - - return useScanDataframe( - resolvedScansDir && scanPath && scanner.data - ? { scansDir: resolvedScansDir, scanPath, scanner: scanner.data } - : skipToken - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanResultData.ts b/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanResultData.ts deleted file mode 100644 index aa9cb7c9c..000000000 --- a/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanResultData.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ColumnTable } from "arquero"; -import { useEffect, useMemo, useState } from "react"; - -import { AsyncData, data, loading } from "../../utils/asyncData"; -import { ScanResultData } from "../types"; -import { parseScanResultData } from "../utils/arrowHelpers"; - -import { useSelectedScanDataframe } from "./useSelectedScanDataframe"; - -export const useSelectedScanResultData = ( - scanResultUuid: string | undefined -): AsyncData => { - const { data: columnTable } = useSelectedScanDataframe(); - return useScanResultData(columnTable, scanResultUuid); -}; - -const useScanResultData = ( - // TODO: We need `| undefined` both on the input param as well as on the output - // in order to honor the rules of hooks when the caller doesn't YET have the uuid. - // Better would be to refactor the parent so that it doesn't even render until - // it has the params so that it can avoid the hook call altogether. - columnTable: ColumnTable | undefined, - rowIdentifier: string | undefined -): AsyncData => { - const [scanResultData, setScanResultData] = useState< - ScanResultData | undefined - >(undefined); - const [isLoading, setIsLoading] = useState(false); - - const filtered = useMemo((): ColumnTable | undefined => { - // Not a valid index - if (!rowIdentifier || !columnTable) { - return undefined; - } - - // Empty table - if (columnTable.columnNames().length === 0) { - return undefined; - } - - const filtered = columnTable - .params({ targetIdentifier: rowIdentifier }) - .filter( - (d: { identifier: string }, $: { targetIdentifier: string }) => - d.identifier === $.targetIdentifier - ); - - if (filtered.numRows() === 0) { - return undefined; - } - - return filtered; - }, [columnTable, rowIdentifier]); - - useEffect(() => { - if (!filtered) { - // TODO: lint react-hooks/set-state-in-effect - consider if fixing this violation makes sense - /* eslint-disable react-hooks/set-state-in-effect */ - setScanResultData(undefined); - setIsLoading(false); - /* eslint-enable react-hooks/set-state-in-effect */ - return; - } - - let cancelled = false; - setIsLoading(true); - - const run = async () => { - try { - const result = await parseScanResultData(filtered); - if (!cancelled) { - setScanResultData(result); - setIsLoading(false); - } - } catch (error) { - if (!cancelled) { - console.error("Error parsing scanner data:", error); - setScanResultData(undefined); - setIsLoading(false); - } - } - }; - - void run(); - - return () => { - cancelled = true; - }; - }, [filtered]); - - return isLoading ? loading : data(scanResultData); -}; diff --git a/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanResultInputData.ts b/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanResultInputData.ts deleted file mode 100644 index 71f4c74c4..000000000 --- a/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanResultInputData.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; - -import { AsyncData } from "../../utils/asyncData"; -import { useScanDataframeInput } from "../server/useScanDataframeInput"; -import { ScanResultInputData } from "../types"; - -import { useScanRoute } from "./useScanRoute"; -import { useSelectedScanner } from "./useSelectedScanner"; - -export const useSelectedScanResultInputData = ( - scanUuid?: string -): AsyncData => { - const { resolvedScansDir, scanPath } = useScanRoute(); - - const scanner = useSelectedScanner(); - - return useScanDataframeInput( - resolvedScansDir && scanPath && scanner.data && scanUuid - ? { - scansDir: resolvedScansDir, - scanPath, - scanner: scanner.data, - uuid: scanUuid, - } - : skipToken - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanner.ts b/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanner.ts deleted file mode 100644 index 4b51e3a65..000000000 --- a/src/inspect_scout/_view/www/src/app/hooks/useSelectedScanner.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useMemo } from "react"; - -import { useMapAsyncData } from "../../hooks/useMapAsyncData"; -import { useStore } from "../../state/store"; -import { Status } from "../../types/api-types"; -import { AsyncData, data } from "../../utils/asyncData"; - -import { useSelectedScan } from "./useSelectedScan"; - -export const useSelectedScanner = (): AsyncData => { - const selectedScanner = useStore((state) => state.selectedScanner); - // TODO: This is a little bogus since we really don't need to do the server fetch - // if we found the selectedScanner from zustand. Alas, the rules of hooks. - const defaultScanner = useMapAsyncData( - useSelectedScan(), - _get_default_scanner - ); - - const selectedScannerAsyncData = useMemo( - () => (selectedScanner ? data(selectedScanner) : undefined), - [selectedScanner] - ); - - return selectedScannerAsyncData ?? defaultScanner; -}; - -const _get_default_scanner = (s: Status): string => { - const result = s.summary.scanners - ? Object.keys(s.summary.scanners)[0] - : undefined; - if (!result) { - throw new Error("Scan must have a scanner"); - } - return result; -}; diff --git a/src/inspect_scout/_view/www/src/app/hooks/useTranscriptsFilterBarProps.ts b/src/inspect_scout/_view/www/src/app/hooks/useTranscriptsFilterBarProps.ts deleted file mode 100644 index 33fef84bd..000000000 --- a/src/inspect_scout/_view/www/src/app/hooks/useTranscriptsFilterBarProps.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; -import { useCallback, useState } from "react"; - -import { ScalarValue } from "../../api/api"; -import { Condition } from "../../query/types"; -import { useCode } from "../server/useCode"; -import { useTranscriptsColumnValues } from "../server/useTranscriptsColumnValues"; - -import { useFilterConditions } from "./useFilterConditions"; - -interface TranscriptsFilterBarProps { - /** Code representations of current filters (Python, SQL). */ - filterCodeValues: Record | undefined; - /** Autocomplete suggestions for the currently edited column. */ - filterSuggestions: ScalarValue[]; - /** Callback when filter column selection changes. */ - onFilterColumnChange: (columnId: string | null) => void; - /** Combined filter condition from all column filters. */ - condition: Condition | undefined; -} - -/** - * Hook providing all props needed for TranscriptFilterBar. - * Manages filter editing state, code generation, and autocomplete suggestions. - */ -export const useTranscriptsFilterBarProps = ( - transcriptsDir: string | undefined -): TranscriptsFilterBarProps => { - const [editingColumnId, setEditingColumnId] = useState(null); - - const condition = useFilterConditions(); - const otherColumnsFilter = useFilterConditions(editingColumnId ?? undefined); - - const { data: filterCodeValues } = useCode(condition ?? skipToken); - - const { data: filterSuggestions } = useTranscriptsColumnValues( - editingColumnId && transcriptsDir - ? { - location: transcriptsDir, - column: editingColumnId, - filter: otherColumnsFilter, - } - : skipToken - ); - - const onFilterColumnChange = useCallback( - (columnId: string | null) => setEditingColumnId(columnId), - [] - ); - - return { - filterCodeValues, - filterSuggestions: filterSuggestions ?? [], - onFilterColumnChange, - condition, - }; -}; diff --git a/src/inspect_scout/_view/www/src/app/project/ProjectPanel.module.css b/src/inspect_scout/_view/www/src/app/project/ProjectPanel.module.css deleted file mode 100644 index dc5b21e79..000000000 --- a/src/inspect_scout/_view/www/src/app/project/ProjectPanel.module.css +++ /dev/null @@ -1,176 +0,0 @@ -.container { - height: 100%; - display: grid; - grid-template-rows: max-content auto 1fr; -} - -.container > .headerRow { - grid-row: 1; -} - -.container > .conflictBanner { - grid-row: 2; -} - -.container > .loading, -.container > .error { - grid-row: 3; -} - -.container > .splitLayout { - grid-row: 3; -} - -.headerRow { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 1.25rem 0.75rem 1rem; - border-bottom: 1px solid var(--vscode-widget-border); -} - -.header { - font-size: 16px; - font-weight: 600; - color: var(--vscode-foreground); -} - -.detail { - font-size: 12px; - color: var(--vscode-descriptionForeground); -} - -.conflictBanner { - background: var(--vscode-inputValidation-warningBackground); - border: 1px solid var(--vscode-inputValidation-warningBorder); - padding: 8px 12px; - margin: 0.5rem 1rem; - border-radius: 4px; - display: flex; - justify-content: space-between; - align-items: center; - font-size: 13px; - color: var(--vscode-foreground); -} - -.conflictActions { - display: flex; - gap: 8px; -} - -.splitLayout { - display: flex; - min-height: 0; - gap: 1rem; - padding-left: 1rem; - padding-top: 1rem; -} - -.treeNav { - width: 160px; - flex-shrink: 0; - overflow-y: auto; -} - -.navList { - list-style: none; - margin: 0; - padding: 0; -} - -.navListItem { - list-style: none; - margin: 0; - padding: 0; -} - -.navGroup { - font-size: 12px; - font-weight: 600; - color: var(--vscode-foreground); - padding: 6px 8px 4px 8px; - text-transform: uppercase; - letter-spacing: 0.03em; -} - -.navGroup:not(:first-child) { - margin-top: 0.75rem; -} - -.navItem { - display: block; - width: 100%; - padding: 4px 8px 4px 16px; - font-size: 13px; - color: var(--vscode-foreground); - background: none; - border: none; - text-align: left; - cursor: pointer; - border-radius: 3px; -} - -.navItem:hover { - background: var(--vscode-list-hoverBackground); -} - -.navItem:focus { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; -} - -.scrollContent { - flex: 1; - min-width: 0; - overflow-y: auto; - padding-right: 1rem; -} - -/* Make text fields wider */ -.field :global(vscode-textfield) { - width: 400px !important; -} - -.field :global(vscode-textfield[type="number"]) { - width: 250px !important; -} - -.field :global(vscode-textarea) { - width: 400px !important; -} - -.field :global(vscode-single-select) { - width: 250px !important; -} - -.section { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-bottom: 1.5rem; -} - -.sectionHeader { - font-size: 13px; - font-weight: 600; - color: var(--vscode-foreground); - padding: 0.35rem 0.75rem 0.35rem 1rem; - margin-left: -0.75rem; - background: var(--vscode-sideBarSectionHeader-background); - border-radius: 3px; -} - -.field { - display: flex; - flex-direction: column; -} - -.loading, -.error { - font-size: 13px; - color: var(--vscode-foreground); -} - -.error { - color: var(--vscode-errorForeground); -} diff --git a/src/inspect_scout/_view/www/src/app/project/ProjectPanel.tsx b/src/inspect_scout/_view/www/src/app/project/ProjectPanel.tsx deleted file mode 100644 index 729555773..000000000 --- a/src/inspect_scout/_view/www/src/app/project/ProjectPanel.tsx +++ /dev/null @@ -1,417 +0,0 @@ -import { useQueryClient } from "@tanstack/react-query"; -import { - VscodeButton, - VscodeFormHelper, -} from "@vscode-elements/react-elements"; -import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useBlocker } from "react-router-dom"; - -import { ApiError } from "../../api/request"; -import { Modal } from "../../components/Modal"; -import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import { AppConfig, ProjectConfigInput } from "../../types/api-types"; -import { appAliasedPath } from "../server/useAppConfig"; -import { - useProjectConfig, - useUpdateProjectConfig, -} from "../server/useProjectConfig"; - -import { - computeConfigToSave, - configsEqual, - deepCopy, - initializeEditedConfig, -} from "./configUtils"; -import styles from "./ProjectPanel.module.css"; -import { SettingsContent } from "./SettingsContent"; - -interface ProjectPanelProps { - config: AppConfig; -} - -// Navigation structure for the settings panel -interface NavItem { - id: string; - label: string; -} - -interface NavSection { - group: string; - items: NavItem[]; -} - -const NAV_SECTIONS: NavSection[] = [ - { - group: "General", - items: [ - { id: "locations", label: "Locations" }, - { id: "scanning", label: "Scanning" }, - { id: "concurrency", label: "Concurrency" }, - { id: "miscellaneous", label: "Miscellaneous" }, - ], - }, - { - group: "Model", - items: [ - { id: "model", label: "Model" }, - { id: "connection", label: "Connection" }, - { id: "generation", label: "Generation" }, - { id: "reasoning", label: "Reasoning" }, - { id: "cache", label: "Cache" }, - { id: "batch", label: "Batch" }, - ], - }, -]; - -export const ProjectPanel: FC = ({ config }) => { - useDocumentTitle("Project"); - - const containerRef = useRef(null); - const scrollContentRef = useRef(null); - const focusedFieldIdRef = useRef(null); - const saveRef = useRef<() => void>(() => {}); - // Track the etag from our own saves to skip re-initialization - const lastSavedEtagRef = useRef(null); - - // Capture focused element ID on mousedown (before click moves focus to button) - // We store the ID since React may recreate the DOM element - const handleSaveMouseDown = useCallback(() => { - const el = document.activeElement as HTMLElement; - if (el && el !== document.body && el.id) { - focusedFieldIdRef.current = el.id; - } else { - focusedFieldIdRef.current = null; - } - }, []); - - // Scroll to section - only scrolls within the scrollContent container - const scrollToSection = useCallback((sectionId: string) => { - const container = scrollContentRef.current; - const element = document.getElementById(sectionId); - if (container && element) { - const containerRect = container.getBoundingClientRect(); - const elementRect = element.getBoundingClientRect(); - const scrollTop = - container.scrollTop + (elementRect.top - containerRect.top); - container.scrollTo({ top: scrollTop, behavior: "smooth" }); - } - }, []); - - const queryClient = useQueryClient(); - const { loading, error, data } = useProjectConfig(); - const mutation = useUpdateProjectConfig(); - - // Ctrl/Cmd+S keyboard shortcut to save - // Always handle since project panel is the only active UI when visible - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === "s") { - e.preventDefault(); - // Guard against double-save during pending mutation - if (!mutation.isPending) { - // Capture focused element ID for restoration after save - const el = document.activeElement as HTMLElement; - if (el && el !== document.body && el.id) { - focusedFieldIdRef.current = el.id; - } else { - focusedFieldIdRef.current = null; - } - saveRef.current(); - } - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [mutation.isPending]); - - const [editedConfig, setEditedConfig] = - useState | null>(null); - const [originalConfig, setOriginalConfig] = - useState | null>(null); - const [conflictError, setConflictError] = useState(false); - - // Initialize config state when data loads - // Skips re-init if this is our own save (etag matches what we just saved) - useEffect(() => { - if (!data) return; - - // If editedConfig is null (initial load or after reload), initialize - if (!editedConfig) { - const initialized = initializeEditedConfig( - data.config as ProjectConfigInput - ); - // TODO: lint react-hooks/set-state-in-effect - consider if fixing this violation makes sense - /* eslint-disable react-hooks/set-state-in-effect */ - setEditedConfig(initialized); - setOriginalConfig(deepCopy(initialized)); - /* eslint-enable react-hooks/set-state-in-effect */ - lastSavedEtagRef.current = data.etag; - return; - } - - // If etag matches what we just saved, skip re-init (this is our own save) - if (data.etag === lastSavedEtagRef.current) { - return; - } - - // Etag changed unexpectedly - reinitialize - // (With current flow this shouldn't happen since external changes - // are detected via 412 on save, but handle it just in case) - const initialized = initializeEditedConfig( - data.config as ProjectConfigInput - ); - setEditedConfig(initialized); - setOriginalConfig(deepCopy(initialized)); - lastSavedEtagRef.current = data.etag; - }, [data, editedConfig]); - - const hasChanges = useMemo(() => { - return !configsEqual(editedConfig, originalConfig); - }, [editedConfig, originalConfig]); - - const blocker = useBlocker( - ({ currentLocation, nextLocation }) => - hasChanges && currentLocation.pathname !== nextLocation.pathname - ); - - const handleConfigChange = useCallback( - (updates: Partial) => { - setEditedConfig((prev) => ({ - ...prev, - ...updates, - })); - }, - [] - ); - - const handleSave = useCallback( - (force = false) => { - if (!data || !editedConfig || !originalConfig) return; - - const updatedConfig = computeConfigToSave( - editedConfig, - originalConfig, - data.config as ProjectConfigInput - ); - - mutation.mutate( - { config: updatedConfig, etag: force ? null : data.etag }, - { - onSuccess: (responseData) => { - setConflictError(false); - lastSavedEtagRef.current = responseData.etag; - setOriginalConfig(deepCopy(editedConfig)); - // Restore focus after save completes (delay to let React finish rendering) - const fieldId = focusedFieldIdRef.current; - setTimeout(() => { - if (fieldId) { - const field = document.getElementById(fieldId); - if (field) { - field.focus(); - } - } - }, 100); - }, - onError: (err) => { - if (err instanceof ApiError && err.status === 412) { - setConflictError(true); - } - }, - } - ); - }, - [data, editedConfig, originalConfig, mutation] - ); - - // Keep saveRef updated for keyboard shortcut - useEffect(() => { - saveRef.current = () => handleSave(false); - }, [handleSave]); - - const handleSaveWithFocusRestore = (force = false) => { - if (!data || !editedConfig || !originalConfig) return; - - const updatedConfig = computeConfigToSave( - editedConfig, - originalConfig, - data.config as ProjectConfigInput - ); - - mutation.mutate( - { config: updatedConfig, etag: force ? null : data.etag }, - { - onSuccess: (responseData) => { - setConflictError(false); - lastSavedEtagRef.current = responseData.etag; - setOriginalConfig(deepCopy(editedConfig)); - // Restore focus after the click event fully completes (delay to let React finish rendering) - const fieldId = focusedFieldIdRef.current; - setTimeout(() => { - if (fieldId) { - const field = document.getElementById(fieldId); - if (field) { - field.focus(); - } - } - }, 100); - }, - onError: (err) => { - if (err instanceof ApiError && err.status === 412) { - setConflictError(true); - } - }, - } - ); - }; - - const handleSaveAndNavigate = () => { - if (!data || !editedConfig || !originalConfig) { - blocker.proceed?.(); - return; - } - - const updatedConfig = computeConfigToSave( - editedConfig, - originalConfig, - data.config as ProjectConfigInput - ); - - mutation.mutate( - { config: updatedConfig, etag: data.etag }, - { - onSuccess: (responseData) => { - setConflictError(false); - lastSavedEtagRef.current = responseData.etag; - setOriginalConfig(deepCopy(editedConfig)); - blocker.proceed?.(); - }, - onError: (err) => { - if (err instanceof ApiError && err.status === 412) { - setConflictError(true); - } - blocker.reset?.(); - }, - } - ); - }; - - const handleReload = () => { - setConflictError(false); - setEditedConfig(null); - setOriginalConfig(null); - // Reset expected etag so the init effect will re-initialize from server - lastSavedEtagRef.current = null; - void queryClient.invalidateQueries({ queryKey: ["project-config-inv"] }); - }; - - return ( -
- {/* Header */} -
-
-
Project Settings
-
- {appAliasedPath(config, config.project_dir)}/scout.yaml -
-
- handleSaveWithFocusRestore(false)} - > - {mutation.isPending ? "Saving..." : "Save Changes"} - -
- - {/* Conflict Banner */} - {conflictError && ( -
- Configuration was modified externally. -
- - Discard My Changes - - handleSaveWithFocusRestore(true)}> - Keep My Changes - -
-
- )} - - {/* Loading State */} - {loading &&
Loading...
} - - {/* Error State */} - {error && ( -
- Error loading config: {error.message} -
- )} - - {/* Split Layout: Tree + Content */} - {editedConfig && ( -
- {/* Navigation */} - - - {/* Scrollable Content */} -
- - Project settings provide default options for scans run from the - project directory. You can override some or all of the defaults - for each scan using command line parameters or a scan job config - file. - - -
-
- )} - - {/* Unsaved Changes Modal */} - blocker.reset?.()} - title="Unsaved Changes" - footer={ - <> - blocker.proceed?.()}> - Don't Save - - blocker.reset?.()}> - Cancel - - - Save - - - } - > - You have unsaved changes. Do you want to save before leaving? - -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/project/SettingsContent.tsx b/src/inspect_scout/_view/www/src/app/project/SettingsContent.tsx deleted file mode 100644 index 8b405acd7..000000000 --- a/src/inspect_scout/_view/www/src/app/project/SettingsContent.tsx +++ /dev/null @@ -1,715 +0,0 @@ -import { - VscodeCheckbox, - VscodeFormHelper, - VscodeLabel, - VscodeTextfield, -} from "@vscode-elements/react-elements"; -import { FC, useCallback, useEffect, useRef, useState } from "react"; - -import { - BatchConfig, - CachePolicy, - GenerateConfigInput, - ProjectConfigInput, -} from "../../types/api-types"; -import { STABLE_EMPTY_OBJECT } from "../../utils/react"; - -import { - KeyValueField, - NumberField, - SelectField, - TextField, -} from "./components/FormFields"; -import { filterNullValues } from "./configUtils"; -import { useBatchConfig, useNestedConfig } from "./hooks/useNestedConfig"; -import styles from "./ProjectPanel.module.css"; - -// Constants for select options -const LOG_LEVELS = [ - "debug", - "http", - "sandbox", - "info", - "warning", - "error", - "critical", - "notset", -] as const; - -const VERBOSITY_OPTIONS = ["low", "medium", "high"] as const; -const EFFORT_OPTIONS = ["low", "medium", "high"] as const; -const REASONING_EFFORT_OPTIONS = [ - "none", - "minimal", - "low", - "medium", - "high", - "xhigh", -] as const; -const REASONING_SUMMARY_OPTIONS = [ - "none", - "concise", - "detailed", - "auto", -] as const; -const REASONING_HISTORY_OPTIONS = ["none", "all", "last", "auto"] as const; - -// Cache expiry validation: must be a number followed by M, H, D, or W -const CACHE_EXPIRY_PATTERN = /^\d+[MHDWmhdw]$/; - -function validateCacheExpiry(value: string | null): string | null { - if (!value) return null; // Empty is valid (will use default) - if (CACHE_EXPIRY_PATTERN.test(value)) return null; - return "Invalid format. Use a number followed by M, H, D, or W (e.g., 30M, 24H, 7D, 1W)"; -} - -export interface SettingsContentProps { - config: Partial; - onChange: (updates: Partial) => void; -} - -export const SettingsContent: FC = ({ - config, - onChange, -}) => { - const generateConfig: GenerateConfigInput = - config.generate_config ?? STABLE_EMPTY_OBJECT; - - // Helper to update generate_config fields - const updateGenerateConfig = useCallback( - (updates: Partial) => { - const merged = { - ...filterNullValues(generateConfig), - ...updates, - }; - - // Filter out null values from the merged result - const cleaned = filterNullValues(merged); - - // If no content remains, set generate_config to null - const hasContent = Object.keys(cleaned).length > 0; - - onChange({ - generate_config: hasContent ? cleaned : null, - }); - }, - [generateConfig, onChange] - ); - - // Cache config hook - const cache = useNestedConfig( - generateConfig.cache, - useCallback( - (value) => updateGenerateConfig({ cache: value }), - [updateGenerateConfig] - ) - ); - - // Batch config hook - const batch = useBatchConfig( - generateConfig.batch, - useCallback( - (value) => updateGenerateConfig({ batch: value }), - [updateGenerateConfig] - ) - ); - - // Refs for scrolling options into view when enabled - const cacheOptionsRef = useRef(null); - const batchOptionsRef = useRef(null); - - // Helper to scroll parent container to bottom by finding scrollable ancestor - const scrollToBottom = useCallback((element: HTMLElement | null) => { - if (!element) return; - // Find scrollable parent by checking overflow property - let scrollContainer = element.parentElement; - while (scrollContainer) { - const overflow = getComputedStyle(scrollContainer).overflowY; - if (overflow === "auto" || overflow === "scroll") { - scrollContainer.scrollTo({ - top: scrollContainer.scrollHeight, - behavior: "smooth", - }); - return; - } - scrollContainer = scrollContainer.parentElement; - } - }, []); - - // Handle scroll after cache is enabled (called from checkbox handler) - const handleCacheEnabled = useCallback(() => { - // Delay scroll to not interfere with focus - setTimeout(() => { - scrollToBottom(cacheOptionsRef.current); - }, 150); - }, [scrollToBottom]); - - // Handle scroll after batch is enabled (called from checkbox handler) - const handleBatchEnabled = useCallback(() => { - // Delay scroll to not interfere with focus - setTimeout(() => { - scrollToBottom(batchOptionsRef.current); - }, 150); - }, [scrollToBottom]); - - // ===== General Section Helpers ===== - - // Tags field - use local state to allow typing commas/spaces freely - // but also update config on input so Ctrl+S saves correctly - const [tagsText, setTagsText] = useState(() => - Array.isArray(config.tags) ? config.tags.join(", ") : (config.tags ?? "") - ); - - // Sync local state when config changes externally (e.g., after save) - useEffect(() => { - const configValue = Array.isArray(config.tags) - ? config.tags.join(", ") - : (config.tags ?? ""); - // Only sync if the parsed values differ (avoids cursor jump while typing) - const currentParsed = tagsText - .split(",") - .map((t) => t.trim()) - .filter((t) => t.length > 0); - const configParsed = Array.isArray(config.tags) ? config.tags : []; - if (JSON.stringify(currentParsed) !== JSON.stringify(configParsed)) { - // TODO: lint react-hooks/set-state-in-effect - consider if fixing this violation makes sense - // eslint-disable-next-line react-hooks/set-state-in-effect - setTagsText(configValue); - } - }, [config.tags, tagsText]); - - const handleTagsInput = (value: string) => { - setTagsText(value); - // Also update config immediately so Ctrl+S works - const tags = value - .split(",") - .map((t) => t.trim()) - .filter((t) => t.length > 0); - onChange({ tags: tags.length > 0 ? tags : null }); - }; - - const shuffleEnabled = - config.shuffle !== null && config.shuffle !== undefined; - const shuffleSeed = - typeof config.shuffle === "number" ? config.shuffle : null; - - const handleShuffleToggle = (enabled: boolean) => { - onChange({ shuffle: enabled ? true : null }); - }; - - const handleShuffleSeedChange = (value: string) => { - const num = parseInt(value, 10); - onChange({ shuffle: isNaN(num) ? true : num }); - }; - - return ( - <> - {/* ===== LOCATIONS SECTION ===== */} -
-
Locations
- - onChange({ transcripts: v })} - placeholder="Path to transcripts" - /> - - onChange({ scans: v })} - placeholder="Path to scans output" - /> -
- - {/* ===== FILTERING SECTION ===== */} -
-
Scanning
- -
- Filter - - SQL WHERE clause(s) for filtering transcripts. This will constrain - any scan done within the project (i.e. filters applied to individual - scans will be AND combined with this filter). - - - onChange({ - filter: (e.target as HTMLInputElement).value || undefined, - }) - } - placeholder="Filter expression" - spellCheck={false} - autocomplete="off" - /> -
- - onChange({ limit: v })} - placeholder="No limit" - /> - -
- Shuffle - - Shuffle the order of transcripts (optionally specify a seed) - -
- - handleShuffleToggle((e.target as HTMLInputElement).checked) - } - > - Enabled - - {shuffleEnabled && ( - - handleShuffleSeedChange((e.target as HTMLInputElement).value) - } - placeholder="Seed (optional)" - style={{ width: "120px" }} - spellCheck={false} - autocomplete="off" - /> - )} -
-
-
- - {/* ===== CONCURRENCY SECTION ===== */} -
-
Concurrency
- - onChange({ max_transcripts: v })} - placeholder="25" - /> - - onChange({ max_processes: v })} - placeholder="4" - /> -
- - {/* ===== MISCELLANEOUS SECTION ===== */} -
-
Miscellaneous
- -
- Tags - - One or more tags to apply to scans (comma-separated) - - - handleTagsInput((e.target as HTMLInputElement).value) - } - placeholder="tag1, tag2, tag3" - spellCheck={false} - autocomplete="off" - /> -
- - - onChange({ metadata: v as Record | null }) - } - placeholder="key=value" - /> - - onChange({ log_level: v })} - defaultLabel="Default (warning)" - /> -
- - {/* ===== MODEL SECTION ===== */} -
-
Model
- - Model configuration specifies the defaults for the model used by the - LLM scanner, as well as the model returned by calls to{" "} - get_model() in custom scanners. - - - onChange({ model: v })} - placeholder="e.g., openai/gpt-5" - /> - - onChange({ model_base_url: v })} - placeholder="API base URL" - /> - - onChange({ model_args: v })} - placeholder="key=value or /path/to/config.yaml" - /> -
- - {/* ===== CONNECTION SECTION ===== */} -
-
Connection
- - updateGenerateConfig({ max_connections: v })} - placeholder="Default" - /> - - updateGenerateConfig({ max_retries: v })} - placeholder="Unlimited" - /> - - updateGenerateConfig({ timeout: v })} - placeholder="No timeout" - /> - - updateGenerateConfig({ attempt_timeout: v })} - placeholder="No timeout" - /> -
- - {/* ===== GENERATION SECTION ===== */} -
-
Generation
- - updateGenerateConfig({ max_tokens: v })} - placeholder="Model default" - /> - - updateGenerateConfig({ temperature: v })} - placeholder="Model default" - step={0.1} - /> - - updateGenerateConfig({ top_p: v })} - placeholder="Model default" - step={0.1} - /> - - updateGenerateConfig({ top_k: v })} - placeholder="Model default" - /> - - updateGenerateConfig({ frequency_penalty: v })} - placeholder="Model default" - step={0.1} - /> - - updateGenerateConfig({ presence_penalty: v })} - placeholder="Model default" - step={0.1} - /> - - updateGenerateConfig({ seed: v })} - placeholder="Random" - /> - - updateGenerateConfig({ verbosity: v })} - /> - - updateGenerateConfig({ effort: v })} - /> -
- - {/* ===== REASONING SECTION ===== */} -
-
Reasoning
- - updateGenerateConfig({ reasoning_effort: v })} - /> - - updateGenerateConfig({ reasoning_tokens: v })} - placeholder="Model default" - /> - - updateGenerateConfig({ reasoning_summary: v })} - /> - - updateGenerateConfig({ reasoning_history: v })} - /> -
- - {/* ===== CACHE SECTION ===== */} -
-
Cache
- -
- { - const checked = (e.target as HTMLInputElement).checked; - cache.setEnabled(checked); - if (checked) handleCacheEnabled(); - }} - > - Enable Caching - - - Cache model responses to improve performance and reduce API costs - -
- - {cache.enabled && ( -
- cache.updateConfig({ expiry: v })} - placeholder="1W (default)" - validate={validateCacheExpiry} - /> - -
- Per Epoch - - Maintain separate cache entries per epoch - - - cache.updateConfig({ - per_epoch: (e.target as HTMLInputElement).checked, - }) - } - > - Enabled - -
-
- )} -
- - {/* ===== BATCH SECTION ===== */} -
-
Batch
- -
- { - const checked = (e.target as HTMLInputElement).checked; - batch.setEnabled(checked); - if (checked) handleBatchEnabled(); - }} - > - Enable Batch Processing - - - Process multiple requests in batches for improved throughput - -
- - {batch.enabled && ( -
- batch.updateConfig({ size: v })} - placeholder="Default" - /> - - batch.updateConfig({ max_size: v })} - placeholder="No limit" - /> - - batch.updateConfig({ max_batches: v })} - placeholder="No limit" - /> - - batch.updateConfig({ send_delay: v })} - placeholder="0" - step={0.1} - /> - - batch.updateConfig({ tick: v })} - placeholder="Default" - step={0.1} - /> - - - batch.updateConfig({ max_consecutive_check_failures: v }) - } - placeholder="No limit" - /> -
- )} -
- - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/project/components/FormFields.tsx b/src/inspect_scout/_view/www/src/app/project/components/FormFields.tsx deleted file mode 100644 index 26fd1c02e..000000000 --- a/src/inspect_scout/_view/www/src/app/project/components/FormFields.tsx +++ /dev/null @@ -1,369 +0,0 @@ -import { - VscodeFormHelper, - VscodeLabel, - VscodeOption, - VscodeSingleSelect, - VscodeTextarea, - VscodeTextfield, -} from "@vscode-elements/react-elements"; -import { FC, ReactNode, useEffect, useState } from "react"; - -import styles from "../ProjectPanel.module.css"; - -// Helper to extract input value with proper typing -function getInputValue(e: Event): string { - return (e.target as HTMLInputElement).value; -} - -function getSelectValue(e: Event): string { - return (e.target as HTMLSelectElement).value; -} - -// Helper to disable spellcheck on web component shadow DOM elements -function createSpellcheckRef(selector: "input" | "textarea") { - return (el: HTMLElement | null) => { - if (!el) return; - el.setAttribute("spellcheck", "false"); - const shadowEl = el.shadowRoot?.querySelector(selector); - if (shadowEl) { - shadowEl.setAttribute("spellcheck", "false"); - } - }; -} - -// ===== TextField Component ===== -interface TextFieldProps { - id?: string; - label: string; - helper?: ReactNode; - value: string | null | undefined; - onChange: (value: string | null) => void; - placeholder?: string; - disabled?: boolean; - /** Optional validation function. Returns error message if invalid, null if valid. */ - validate?: (value: string | null) => string | null; -} - -export const TextField: FC = ({ - id, - label, - helper, - value, - onChange, - placeholder, - disabled, - validate, -}) => { - // Debounce validation errors to avoid flashing during typing - const [debouncedError, setDebouncedError] = useState(null); - const errorMessage = validate && !disabled ? validate(value ?? null) : null; - - useEffect(() => { - if (errorMessage) { - const timer = setTimeout(() => setDebouncedError(errorMessage), 1000); - return () => clearTimeout(timer); - } else { - // Clear error immediately when input becomes valid - // TODO: lint react-hooks/set-state-in-effect - consider if fixing this violation makes sense - // eslint-disable-next-line react-hooks/set-state-in-effect - setDebouncedError(null); - } - }, [errorMessage]); - - return ( -
- {label} - {helper && {helper}} - onChange(getInputValue(e) || null)} - placeholder={placeholder} - spellCheck={false} - autocomplete="off" - /> - {debouncedError && ( - - {debouncedError} - - )} -
- ); -}; - -// ===== TextAreaField Component ===== -interface TextAreaFieldProps { - id?: string; - label: string; - helper?: ReactNode; - value: string | null | undefined; - onChange: (value: string | null) => void; - placeholder?: string; - disabled?: boolean; - rows?: number; -} - -export const TextAreaField: FC = ({ - id, - label, - helper, - value, - onChange, - placeholder, - disabled, - rows = 3, -}) => { - return ( -
- {label} - {helper && {helper}} - onChange(getInputValue(e) || null)} - placeholder={placeholder} - rows={rows} - spellCheck={false} - autocomplete="off" - /> -
- ); -}; - -// ===== KeyValueField Component ===== -// Handles key=value pairs (one per line) or plain string values - -/** - * Convert an object or string to key=value lines for display. - */ -export function objectToKeyValueLines( - value: Record | string | null | undefined -): string { - if (!value) return ""; - if (typeof value === "string") return value; - return Object.entries(value) - .map(([k, v]) => `${k}=${String(v)}`) - .join("\n"); -} - -/** - * Parse key=value lines into an object, or return as string if it looks like a path. - * Numeric values are preserved as numbers. - */ -export function parseKeyValueLines( - text: string | null -): Record | string | null { - if (!text?.trim()) return null; - - // If it looks like a file path, return as string - const trimmed = text.trim(); - if ( - trimmed.startsWith("/") || - trimmed.startsWith("./") || - trimmed.startsWith("~") || - /^[a-zA-Z]:\\/.test(trimmed) // Windows path - ) { - return trimmed; - } - - // Parse as key=value pairs - const result: Record = {}; - for (const line of text.split("\n")) { - const lineTrimmed = line.trim(); - if (!lineTrimmed) continue; - const eqIndex = lineTrimmed.indexOf("="); - if (eqIndex > 0) { - const key = lineTrimmed.slice(0, eqIndex).trim(); - const val = lineTrimmed.slice(eqIndex + 1).trim(); - if (key) { - // Try to parse as number if it looks numeric - const num = Number(val); - if (val !== "" && !isNaN(num)) { - result[key] = num; - } else { - result[key] = val; - } - } - } - } - - return Object.keys(result).length > 0 ? result : null; -} - -interface KeyValueFieldProps { - id?: string; - label: string; - helper?: ReactNode; - value: Record | string | null | undefined; - onChange: (value: Record | string | null) => void; - placeholder?: string; - disabled?: boolean; - rows?: number; -} - -export const KeyValueField: FC = ({ - id, - label, - helper, - value, - onChange, - placeholder = "key=value", - disabled, - rows = 3, -}) => { - // Use local state to allow free typing without losing input - const [text, setText] = useState(() => objectToKeyValueLines(value)); - - // Sync local state when value changes externally (e.g., after save) - useEffect(() => { - const configText = objectToKeyValueLines(value); - // Only sync if parsed values differ (avoids cursor jump while typing) - const currentParsed = parseKeyValueLines(text); - const valueParsed = parseKeyValueLines(configText); - if (JSON.stringify(currentParsed) !== JSON.stringify(valueParsed)) { - setText(configText); - } - // TODO: lint react-hooks/exhaustive-deps - review this - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value]); - - const handleInput = (newText: string) => { - setText(newText); - // Also update config immediately so Ctrl+S works - onChange(parseKeyValueLines(newText)); - }; - - return ( -
- {label} - {helper && {helper}} - handleInput(getInputValue(e))} - placeholder={placeholder} - rows={rows} - spellCheck={false} - autocomplete="off" - /> -
- ); -}; - -// ===== NumberField Component ===== -interface NumberFieldProps { - id?: string; - label: string; - helper?: ReactNode; - value: number | null | undefined; - onChange: (value: number | null) => void; - placeholder?: string; - disabled?: boolean; - step?: number; -} - -export const NumberField: FC = ({ - id, - label, - helper, - value, - onChange, - placeholder, - disabled, - step, -}) => { - const handleInput = (e: Event) => { - const val = getInputValue(e); - const num = step ? parseFloat(val) : parseInt(val, 10); - onChange(isNaN(num) ? null : num); - }; - - return ( -
- {label} - {helper && {helper}} - -
- ); -}; - -// ===== SelectField Component ===== -interface SelectFieldProps { - id?: string; - label: string; - helper?: ReactNode; - value: T | null | undefined; - options: readonly T[]; - onChange: (value: T | null) => void; - disabled?: boolean; - defaultLabel?: string; -} - -export function SelectField({ - id, - label, - helper, - value, - options, - onChange, - disabled, - defaultLabel = "Default", -}: SelectFieldProps): ReactNode { - return ( -
- {label} - {helper && {helper}} - { - const val = getSelectValue(e); - onChange(val ? (val as T) : null); - }} - > - {defaultLabel} - {options.map((opt) => ( - - {opt} - - ))} - -
- ); -} - -// ===== Field Component ===== -interface FieldProps { - label: string; - helper?: ReactNode; - children: ReactNode; -} - -export function Field({ label, helper, children }: FieldProps): ReactNode { - return ( -
- {label} - {helper && {helper}} - {children} -
- ); -} diff --git a/src/inspect_scout/_view/www/src/app/project/configUtils.ts b/src/inspect_scout/_view/www/src/app/project/configUtils.ts deleted file mode 100644 index e7c4645f2..000000000 --- a/src/inspect_scout/_view/www/src/app/project/configUtils.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { ProjectConfigInput } from "../../types/api-types"; - -/** - * Deep equality check for config objects using JSON serialization. - */ -export function configsEqual( - a: Partial | null, - b: Partial | null -): boolean { - if (a === b) return true; - if (!a || !b) return false; - return JSON.stringify(a) === JSON.stringify(b); -} - -/** - * Check if a value is "empty" (null, undefined, or empty object/array). - */ -export function isEmpty(value: unknown): boolean { - if (value === null || value === undefined) return true; - if (Array.isArray(value)) return value.length === 0; - if (typeof value === "object") return Object.keys(value).length === 0; - return false; -} - -/** - * Filter out null and undefined values from an object. - */ -export function filterNullValues>( - obj: T -): Partial { - return Object.fromEntries( - Object.entries(obj).filter(([, v]) => v !== null && v !== undefined) - ) as Partial; -} - -/** - * Deep copy an object using JSON serialization. - * Note: Only works for JSON-serializable types (objects, arrays, primitives). - * Loses functions, Symbols, undefined values. Circular references will throw. - */ -export function deepCopy(obj: T): T { - return JSON.parse(JSON.stringify(obj)); -} - -/** - * Clean nested config (cache/batch) for saving. - * Handles boolean, number, object, null, and undefined values. - */ -function cleanNestedConfig( - edited: Record | boolean | number | null | undefined, - original: Record | boolean | number | null | undefined -): Record | boolean | number | null | undefined { - // Preserve boolean and number values as-is - if (typeof edited === "boolean" || typeof edited === "number") { - return edited; - } - - // If edited is empty, return null if original had content - if (edited === null || edited === undefined) { - if (original !== null && original !== undefined) { - return null; - } - return undefined; - } - - const result: Record = {}; - const originalObj = - typeof original === "object" && original !== null ? original : {}; - - for (const [key, value] of Object.entries(edited)) { - const origValue = originalObj[key]; - const valueChanged = JSON.stringify(value) !== JSON.stringify(origValue); - - if (valueChanged) { - result[key] = value; - } else if (!isEmpty(value)) { - result[key] = value; - } - } - - // If result is empty but original had content, return true (enabled state) - if (Object.keys(result).length === 0) { - if (original !== null && original !== undefined) { - return true; - } - return undefined; - } - - return result; -} - -/** - * Clean generate_config for saving. - * Handles nested cache/batch configs and removes empty values. - */ -function cleanGenerateConfig( - edited: Record | null | undefined, - original: Record | null | undefined -): Record | null | undefined { - // Handle empty edited config - if ( - edited === null || - edited === undefined || - (typeof edited === "object" && Object.keys(edited).length === 0) - ) { - const originalHasContent = - original !== null && - original !== undefined && - typeof original === "object" && - Object.keys(original).length > 0; - if (originalHasContent) { - return null; - } - return undefined; - } - - const result: Record = {}; - const originalObj = original ?? {}; - - for (const key of Object.keys(edited)) { - const editedValue = edited[key]; - const originalValue = originalObj[key]; - - // Handle nested cache/batch configs - if (key === "cache" || key === "batch") { - const cleanedNested = cleanNestedConfig( - editedValue as - | Record - | boolean - | number - | null - | undefined, - originalValue as - | Record - | boolean - | number - | null - | undefined - ); - if (cleanedNested !== undefined) { - result[key] = cleanedNested; - } - continue; - } - - // Handle null/undefined values - if (editedValue === null || editedValue === undefined) { - const originalHadContent = - originalValue !== null && - originalValue !== undefined && - (typeof originalValue !== "object" || - Object.keys(originalValue).length > 0); - if (originalHadContent) { - result[key] = null; - } - continue; - } - - result[key] = editedValue; - } - - if (Object.keys(result).length === 0) { - return undefined; - } - - return result; -} - -/** - * Compute the config to save by comparing edited values against original server state. - * Only includes values that have changed or have content. - */ -export function computeConfigToSave( - edited: Partial, - original: Partial, - serverConfig: ProjectConfigInput -): ProjectConfigInput { - const result: Record = {}; - - const allKeys = new Set([ - ...Object.keys(edited), - ...Object.keys(serverConfig), - ]); - - for (const key of allKeys) { - const editedValue = edited[key as keyof ProjectConfigInput]; - const originalValue = original[key as keyof ProjectConfigInput]; - - // Handle generate_config specially - if (key === "generate_config") { - const cleanedGenConfig = cleanGenerateConfig( - editedValue as Record | null | undefined, - originalValue as Record | null | undefined - ); - if (cleanedGenConfig !== undefined) { - result[key] = cleanedGenConfig; - } - continue; - } - - const valueChanged = - JSON.stringify(editedValue) !== JSON.stringify(originalValue); - - if (valueChanged) { - result[key] = editedValue; - } else if (!isEmpty(editedValue)) { - result[key] = editedValue; - } - } - - return result as ProjectConfigInput; -} - -/** - * Initialize edited config from server config. - * Extracts the relevant fields for editing. - * All fields are normalized to null (not undefined) for consistent comparison. - */ -export function initializeEditedConfig( - serverConfig: ProjectConfigInput -): Partial { - return { - transcripts: serverConfig.transcripts ?? null, - filter: serverConfig.filter ?? null, - scans: serverConfig.scans ?? null, - max_transcripts: serverConfig.max_transcripts ?? null, - max_processes: serverConfig.max_processes ?? null, - limit: serverConfig.limit ?? null, - shuffle: serverConfig.shuffle ?? null, - tags: serverConfig.tags ?? null, - metadata: serverConfig.metadata ?? null, - log_level: serverConfig.log_level ?? null, - model: serverConfig.model ?? null, - model_base_url: serverConfig.model_base_url ?? null, - model_args: serverConfig.model_args ?? null, - generate_config: serverConfig.generate_config ?? null, - }; -} diff --git a/src/inspect_scout/_view/www/src/app/project/hooks/useNestedConfig.ts b/src/inspect_scout/_view/www/src/app/project/hooks/useNestedConfig.ts deleted file mode 100644 index b2d73ac2a..000000000 --- a/src/inspect_scout/_view/www/src/app/project/hooks/useNestedConfig.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { useCallback, useMemo } from "react"; - -import { filterNullValues } from "../configUtils"; - -/** - * Hook for managing nested config sections like cache and batch. - * - * These configs can be: - * - null/undefined (disabled) - * - true (enabled with defaults) - * - number (for batch: simple size value) - * - object (enabled with specific settings) - * - * @param configValue The current config value (from parent generate_config) - * @param updateParent Function to update the parent config with new nested value - */ -export function useNestedConfig>( - configValue: T | boolean | number | null | undefined, - updateParent: (value: T | boolean | null) => void -) { - const enabled = - configValue !== null && configValue !== undefined && configValue !== false; - - const config: Partial = useMemo(() => { - if (typeof configValue === "object" && configValue !== null) { - return { ...configValue }; // Shallow copy - } - return {} as Partial; - }, [configValue]); - - const setEnabled = useCallback( - (newEnabled: boolean) => { - if (newEnabled) { - updateParent(true); - } else { - updateParent(null); - } - }, - [updateParent] - ); - - const updateConfig = useCallback( - (updates: Partial) => { - const existingConfig = - typeof configValue === "object" && configValue !== null - ? filterNullValues(configValue) - : {}; - updateParent({ - ...existingConfig, - ...updates, - } as T); - }, - [configValue, updateParent] - ); - - return { - enabled, - config, - setEnabled, - updateConfig, - }; -} - -/** - * Hook specifically for batch config which can also be a simple number (size). - */ -export function useBatchConfig>( - configValue: T | boolean | number | null | undefined, - updateParent: (value: T | boolean | null) => void -) { - const base = useNestedConfig(configValue, updateParent); - - const simpleBatchSize = typeof configValue === "number" ? configValue : null; - - const updateConfig = useCallback( - (updates: Partial) => { - const existingConfig = - typeof configValue === "object" && configValue !== null - ? filterNullValues(configValue) - : {}; - const size = - typeof configValue === "number" - ? configValue - : (existingConfig as Record).size; - updateParent({ - ...(size !== undefined ? { size } : {}), - ...existingConfig, - ...updates, - } as T); - }, - [configValue, updateParent] - ); - - const currentBatchSize = useMemo(() => { - if (typeof configValue === "object" && configValue !== null) { - return (configValue as Record).size as - | number - | undefined; - } - return simpleBatchSize ?? undefined; - }, [configValue, simpleBatchSize]); - - return { - ...base, - updateConfig, - currentBatchSize, - }; -} diff --git a/src/inspect_scout/_view/www/src/app/runScan/ActiveScanView.tsx b/src/inspect_scout/_view/www/src/app/runScan/ActiveScanView.tsx deleted file mode 100644 index 80874379e..000000000 --- a/src/inspect_scout/_view/www/src/app/runScan/ActiveScanView.tsx +++ /dev/null @@ -1,304 +0,0 @@ -import { FC, useEffect, useState } from "react"; - -import { ErrorPanel } from "../../components/ErrorPanel"; -import { ApplicationIcons } from "../../components/icons"; -import { LoadingBar } from "../../components/LoadingBar"; -import { NoContentsPanel } from "../../components/NoContentsPanel"; -import { - ActiveScanInfo, - ScannerSummary, - ValidationResults, -} from "../../types/api-types"; -import { useActiveScan } from "../server/useActiveScan"; - -import styles from "./RunScanPanel.module.css"; - -const formatMemory = (bytes: number): string => { - const gb = bytes / (1024 * 1024 * 1024); - const formatted = gb.toFixed(1).replace(/\.?0+$/, ""); - return `${formatted} GB`; -}; - -const formatDuration = (seconds: number): string => { - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds % 3600) / 60); - const s = Math.floor(seconds % 60); - if (h > 0) - return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; - return `${m}:${s.toString().padStart(2, "0")}`; -}; - -/** - * Get validation accuracy from pre-computed ValidationResults. - */ -const getValidationScore = ( - validation: ValidationResults | null | undefined -): number | null => { - if (validation?.metrics?.accuracy == null) return null; - return validation.metrics.accuracy; -}; - -const getFirstMetricValue = ( - metrics: Record> | null -): number | null => { - if (!metrics) return null; - const firstNested = Object.values(metrics)[0]; - if (!firstNested) return null; - return Object.values(firstNested)[0] ?? null; -}; - -const getMetricLabel = (scanners: Record): string => { - const names = new Set(); - for (const scanner of Object.values(scanners)) { - if (scanner.metrics) { - const firstNested = Object.values(scanner.metrics)[0]; - if (firstNested) { - const firstKey = Object.keys(firstNested)[0]; - if (firstKey) names.add(firstKey); - } - } - } - return names.size === 1 ? ([...names][0] ?? "metric") : "metric"; -}; - -const ActiveScanCard: FC<{ info: ActiveScanInfo }> = ({ info }) => { - const { metrics, summary } = info; - // Date.now() is intentionally impure here - we need the current time to calculate - // live-updating elapsed/remaining durations. Safe for this client-only component. - // eslint-disable-next-line react-hooks/purity - const [now, setNow] = useState(Date.now()); - - // Update time every second for elapsed/remaining - useEffect(() => { - const interval = setInterval(() => setNow(Date.now()), 1000); - return () => clearInterval(interval); - }, []); - - // Check if any scanner has validations or metrics (use scanner_names for iteration) - const hasValidations = info.scanner_names.some( - (name) => (summary.scanners[name]?.validation?.entries?.length ?? 0) > 0 - ); - const hasMetrics = info.scanner_names.some( - (name) => summary.scanners[name]?.metrics !== null - ); - const metricLabel = hasMetrics ? getMetricLabel(summary.scanners) : ""; - - // Calculate scanner stats (iterate over scanner_names to show all rows from start) - const scannerStats = info.scanner_names.map((name) => { - const scanner = summary.scanners[name]; - const totalTokens = scanner - ? Object.values(scanner.model_usage).reduce( - (sum, usage) => sum + (usage.total_tokens ?? 0), - 0 - ) - : 0; - const tokensPerScan = - scanner && scanner.scans > 0 - ? Math.round(totalTokens / scanner.scans) - : 0; - const validationScore = scanner - ? getValidationScore(scanner.validation) - : null; - const metricValue = scanner - ? getFirstMetricValue(scanner.metrics ?? null) - : null; - return { - name, - scanner, - totalTokens, - tokensPerScan, - validationScore, - metricValue, - }; - }); - - // Progress calculations - const completed = metrics.completed_scans; - const total = info.total_scans; - const progressPct = total > 0 ? (completed / total) * 100 : 0; - const elapsedSec = info.start_time > 0 ? now / 1000 - info.start_time : 0; - const remainingSec = - completed > 0 && total > completed - ? (elapsedSec / completed) * (total - completed) - : 0; - - // Batch processing - const hasBatch = metrics.batch_oldest_created !== null; - const batchAge = hasBatch - ? Math.floor(now / 1000 - (metrics.batch_oldest_created ?? 0)) - : 0; - - return ( -
-
-
- - {info.title || `scan: ${info.scan_id}`} - - {info.config && ( -
{info.config}
- )} -
-
- - {total > 0 && ( -
-
-
-
-
- - {completed.toLocaleString()}/{total.toLocaleString()} - - {formatDuration(elapsedSec)} - {remainingSec > 0 && {formatDuration(remainingSec)}} -
-
- )} - -
-
- - - - - {hasMetrics && ( - - )} - {hasValidations && ( - - )} - - - - - - - - {scannerStats.map( - ({ - name, - scanner, - totalTokens, - tokensPerScan, - validationScore, - metricValue, - }) => ( - - - {hasMetrics && ( - - )} - {hasValidations && ( - - )} - - - - - - ) - )} - -
scanner{metricLabel}validationresultserrorstokens/scantokens
{name} - {metricValue !== null - ? metricValue === Math.floor(metricValue) - ? metricValue.toLocaleString() - : metricValue.toFixed(2) - : "-"} - - {validationScore !== null - ? validationScore.toFixed(2) - : "-"} - - {scanner?.results || "-"} - {scanner?.errors || "-"} - {tokensPerScan ? tokensPerScan.toLocaleString() : "-"} - - {totalTokens ? totalTokens.toLocaleString() : "-"} -
-
- -
-
-
workers
-
- parsing: - {metrics.tasks_parsing} -
-
- scanning: - {metrics.tasks_scanning} -
-
- idle: - {metrics.tasks_idle} -
-
- memory: - {formatMemory(metrics.memory_usage)} -
-
- - {hasBatch && ( -
-
batch processing
-
- pending: - {metrics.batch_pending.toLocaleString()} -
- {metrics.batch_failures > 0 && ( -
- failures: - - {metrics.batch_failures.toLocaleString()} - -
- )} -
- max age: - {formatDuration(batchAge)} -
-
- )} -
-
-
- ); -}; - -interface Props { - scanId: string | undefined; -} - -export const ActiveScanView: FC = ({ scanId }) => { - const { loading, error, data: scanInfo } = useActiveScan(scanId); - - return ( -
- - {error && ( - - )} - {!scanId && !error && ( - - )} - {scanId && !scanInfo && !error && !loading && ( - - )} - {scanInfo && !error && } -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/runScan/DefineScannerSection.tsx b/src/inspect_scout/_view/www/src/app/runScan/DefineScannerSection.tsx deleted file mode 100644 index de6cd71f7..000000000 --- a/src/inspect_scout/_view/www/src/app/runScan/DefineScannerSection.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { - VscodeButton, - VscodeLabel, - VscodeOption, - VscodeSingleSelect, -} from "@vscode-elements/react-elements"; -import { FC, useState } from "react"; - -import { ApplicationIcons } from "../../components/icons"; -import { ScansNavbar } from "../components/ScansNavbar"; -import { TranscriptsNavbar } from "../components/TranscriptsNavbar"; -import { useTranscriptsFilterBarProps } from "../hooks/useTranscriptsFilterBarProps"; -import { useAppConfig } from "../server/useAppConfig"; -import { useScanners } from "../server/useScanners"; -import { useStartScan } from "../server/useStartScan"; -import { TranscriptFilterBar } from "../transcripts/TranscriptFilterBar"; -import { useScansDir } from "../utils/useScansDir"; -import { useTranscriptsDir } from "../utils/useTranscriptsDir"; - -import { LlmScannerParams, LlmScannerParamsValue } from "./LlmScannerParams"; -import styles from "./RunScanPanel.module.css"; -import { ScannerParamsPlaceholder } from "./ScannerParamsPlaceholder"; - -function getSelectValue(e: Event): string { - return (e.target as HTMLSelectElement).value; -} - -interface Props { - onScanStarted: (scanId: string) => void; -} - -const defaultLlmParams: LlmScannerParamsValue = { - question: "", - answerType: "boolean", - excludeSystem: true, - excludeReasoning: false, - excludeToolUsage: false, -}; - -export const DefineScannerSection: FC = ({ onScanStarted }) => { - const [selectedScanner, setSelectedScanner] = useState(null); - const [llmParams, setLlmParams] = useState(defaultLlmParams); - const { loading, data: scanners } = useScanners(); - const mutation = useStartScan(); - - const config = useAppConfig(); - const filter = Array.isArray(config.filter) - ? config.filter.join(" ") - : config.filter; - - const { - displayTranscriptsDir, - resolvedTranscriptsDir, - resolvedTranscriptsDirSource, - setTranscriptsDir, - } = useTranscriptsDir(true); - const { displayScansDir, resolvedScansDirSource, setScansDir } = - useScansDir(true); - - const { filterCodeValues, filterSuggestions, onFilterColumnChange } = - useTranscriptsFilterBarProps(resolvedTranscriptsDir); - - const effectiveScanner = selectedScanner ?? scanners?.[0]?.name; - const selectedScannerInfo = scanners?.find( - (s) => s.name === effectiveScanner - ); - const canRunScan = - effectiveScanner === "inspect_scout/llm_scanner" && - llmParams.question.trim().length > 0 && - !mutation.isPending; - - const handleRunScan = () => { - mutation.mutate( - { - name: "inspect_scout/llm_scanner", - filter: [], - limit: 100, - scanners: [ - { - name: "inspect_scout/llm_scanner", - version: 0, - params: { - question: llmParams.question, - answer: llmParams.answerType, - preprocessor: { - exclude_system: llmParams.excludeSystem, - exclude_reasoning: llmParams.excludeReasoning, - exclude_tool_usage: llmParams.excludeToolUsage, - }, - }, - }, - ], - }, - { onSuccess: (data) => onScanStarted(data.spec.scan_id) } - ); - }; - - return ( - <> - - - - -
-

Define Scanner

- - {/* Scanner Selection */} -
- Type -
- setSelectedScanner(getSelectValue(e))} - disabled={loading} - > - {scanners?.map((s) => ( - - {s.name} - - ))} - - {selectedScannerInfo?.description && ( -
- {selectedScannerInfo.description} -
- )} -
-
- - {/* Scanner Params */} - {effectiveScanner === "inspect_scout/llm_scanner" ? ( - - ) : effectiveScanner ? ( - - ) : null} - - {/* Run Button */} -
- - - Run Scan - -
- start scan status: {mutation.status} -
-
-
- - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/runScan/LlmScannerParams.tsx b/src/inspect_scout/_view/www/src/app/runScan/LlmScannerParams.tsx deleted file mode 100644 index 26e50d9e1..000000000 --- a/src/inspect_scout/_view/www/src/app/runScan/LlmScannerParams.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { - VscodeCheckbox, - VscodeLabel, - VscodeOption, - VscodeSingleSelect, - VscodeTextarea, -} from "@vscode-elements/react-elements"; -import { FC } from "react"; - -import styles from "./RunScanPanel.module.css"; - -function getInputValue(e: Event): string { - return (e.target as HTMLInputElement).value; -} - -function getSelectValue(e: Event): string { - return (e.target as HTMLSelectElement).value; -} - -export interface LlmScannerParamsValue { - question: string; - answerType: "boolean" | "numeric" | "string"; - excludeSystem: boolean; - excludeReasoning: boolean; - excludeToolUsage: boolean; -} - -interface Props { - value: LlmScannerParamsValue; - onChange: (value: LlmScannerParamsValue) => void; -} - -const placeholderByAnswerType = { - boolean: "Enter a yes/no question to ask about each transcript...", - numeric: - "Enter a question that yields a numeric answer for each transcript...", - string: "Enter a question to ask about each transcript...", -} as const; - -export const LlmScannerParams: FC = ({ value, onChange }) => { - const update = (partial: Partial) => - onChange({ ...value, ...partial }); - - return ( -
-
-
- Question - update({ question: getInputValue(e) })} - /> -
-
-
-
- Answer type - - update({ - answerType: getSelectValue(e) as - | "boolean" - | "numeric" - | "string", - }) - } - > - Boolean - Numeric - String - -
-
- Message filter -
- - update({ - excludeSystem: (e.target as HTMLInputElement).checked, - }) - } - > - Exclude system messages - - - update({ - excludeReasoning: (e.target as HTMLInputElement).checked, - }) - } - > - Exclude reasoning content - - - update({ - excludeToolUsage: (e.target as HTMLInputElement).checked, - }) - } - > - Exclude tool usage - -
-
-
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/runScan/RunScanPanel.module.css b/src/inspect_scout/_view/www/src/app/runScan/RunScanPanel.module.css deleted file mode 100644 index 8278131f1..000000000 --- a/src/inspect_scout/_view/www/src/app/runScan/RunScanPanel.module.css +++ /dev/null @@ -1,196 +0,0 @@ -.container { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; -} - -.defineScannerSection { - padding: 1rem; - border-bottom: 1px solid var(--bs-border-color); - flex-shrink: 0; -} - -.sectionTitle { - font-size: 1rem; - font-weight: 600; - margin: 0 0 1rem 0; -} - -.formRow { - display: flex; - gap: 2rem; -} - -.formColumn { - display: flex; - flex-direction: column; - gap: 0.75rem; - flex: 1; -} - -.formGroup { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.scannerRow { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.scannerDescription { - flex: 2; - font-size: 0.75rem; - color: var(--bs-secondary); -} - -.paramsPlaceholder { - padding: 1rem; - color: var(--bs-secondary); - font-size: 0.875rem; -} - -.runScanRow { - display: flex; - align-items: center; - gap: 1rem; - margin-top: 0.75rem; -} - -.checkboxGroup { - display: flex; - gap: 1.5rem; -} - -.scansList { - display: flex; - flex-direction: column; - flex: 1; - padding: 1rem; - gap: 1rem; - overflow: auto; -} - -.card { - border: 1px solid var(--bs-border-color); - border-radius: 0.375rem; - padding: 1rem; - background-color: var(--bs-body-bg); -} - -.header { - margin-bottom: 0.75rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid var(--bs-border-color); -} - -.scanId { - font-weight: 600; - font-family: monospace; -} - -.configLine { - color: var(--bs-secondary); - font-size: 0.75rem; - font-family: monospace; - margin-top: 0.25rem; -} - -.progressSection { - margin-bottom: 0.75rem; -} - -.progressBar { - height: 4px; - background-color: var(--bs-secondary-bg); - border-radius: 2px; - overflow: hidden; -} - -.progressFill { - height: 100%; - background-color: var(--bs-success); - transition: width 0.3s ease; -} - -.progressStats { - display: flex; - gap: 1.5rem; - margin-top: 0.25rem; - font-family: monospace; - font-size: 0.75rem; - color: var(--bs-secondary); -} - -.content { - display: flex; - gap: 2rem; -} - -.mainSection { - flex: 1; -} - -.table { - width: 100%; - border-collapse: collapse; - font-family: monospace; - font-size: 0.875rem; -} - -.table th, -.table td { - padding: 0.25rem 0.75rem; - text-align: left; -} - -.table th { - font-weight: 600; - color: var(--bs-secondary); -} - -.numeric { - text-align: right; -} - -.sidebar { - min-width: 150px; - border-left: 1px solid var(--bs-border-color); - padding-left: 1rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.sidebarSection { - font-family: monospace; - font-size: 0.875rem; -} - -.sidebarTitle { - font-weight: 600; - margin-bottom: 0.5rem; -} - -.stat { - display: flex; - justify-content: space-between; - gap: 1rem; -} - -.stat span:first-child { - color: var(--bs-secondary); -} - -.error { - color: var(--bs-danger); - font-weight: 600; -} - -.mutationStatus { - font-size: 0.7rem; - color: var(--bs-secondary); -} diff --git a/src/inspect_scout/_view/www/src/app/runScan/RunScanPanel.tsx b/src/inspect_scout/_view/www/src/app/runScan/RunScanPanel.tsx deleted file mode 100644 index f118acc5f..000000000 --- a/src/inspect_scout/_view/www/src/app/runScan/RunScanPanel.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { clsx } from "clsx"; -import { FC, useState } from "react"; - -import { useDocumentTitle } from "../../hooks/useDocumentTitle"; - -import { ActiveScanView } from "./ActiveScanView"; -import { DefineScannerSection } from "./DefineScannerSection"; -import styles from "./RunScanPanel.module.css"; - -export const RunScanPanel: FC = () => { - useDocumentTitle("Run Scan"); - - const [scanId, setScanId] = useState(); - return ( -
- - -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/runScan/ScannerParamsPlaceholder.tsx b/src/inspect_scout/_view/www/src/app/runScan/ScannerParamsPlaceholder.tsx deleted file mode 100644 index ed63ed573..000000000 --- a/src/inspect_scout/_view/www/src/app/runScan/ScannerParamsPlaceholder.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { FC } from "react"; - -import styles from "./RunScanPanel.module.css"; - -interface Props { - scannerName: string; -} - -export const ScannerParamsPlaceholder: FC = ({ scannerName }) => ( -
- No parameter editor available for {scannerName} -
-); diff --git a/src/inspect_scout/_view/www/src/app/scan/ScanPanel.module.css b/src/inspect_scout/_view/www/src/app/scan/ScanPanel.module.css deleted file mode 100644 index 1786ff4a4..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/ScanPanel.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.root { - height: 100%; - display: grid; - grid-template-rows: max-content max-content max-content max-content 1fr; -} diff --git a/src/inspect_scout/_view/www/src/app/scan/ScanPanel.tsx b/src/inspect_scout/_view/www/src/app/scan/ScanPanel.tsx deleted file mode 100644 index 94ba8f405..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/ScanPanel.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import clsx from "clsx"; -import React, { useEffect } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { ErrorPanel } from "../../components/ErrorPanel"; -import { ExtendedFindProvider } from "../../components/ExtendedFindProvider"; -import { LoadingBar } from "../../components/LoadingBar"; -import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import { getScannerParam } from "../../router/url"; -import { useStore } from "../../state/store"; -import { ScansNavbar } from "../components/ScansNavbar"; -import { useSelectedScan } from "../hooks/useSelectedScan"; -import { useAppConfig } from "../server/useAppConfig"; -import { useScans } from "../server/useScans"; -import { getScanDisplayName } from "../utils/scan"; -import { useScansDir } from "../utils/useScansDir"; - -import styles from "./ScanPanel.module.css"; -import { ScanPanelBody } from "./ScanPanelBody"; -import { ScanPanelTitle } from "./ScanPanelTitle"; - -export const ScanPanel: React.FC = () => { - const config = useAppConfig(); - const scansDir = config.scans.dir; - const { - displayScansDir, - resolvedScansDir, - resolvedScansDirSource, - setScansDir, - } = useScansDir(true); - // Load server data - const { loading: scansLoading } = useScans(resolvedScansDir); - const { loading: scanLoading, data: selectedScan, error } = useSelectedScan(); - - const loading = scansLoading || scanLoading; - - // Set document title with scan location - useDocumentTitle(getScanDisplayName(selectedScan, scansDir), "Scans"); - - // Clear scan state from the store on mount - const clearScanState = useStore((state) => state.clearScanState); - useEffect(() => { - clearScanState(); - // TODO: lint react-hooks/exhaustive-deps - should we just add clearScanState to the dependencies - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Sync URL query param with store state - const [searchParams] = useSearchParams(); - const setSelectedScanner = useStore((state) => state.setSelectedScanner); - useEffect(() => { - const scannerParam = getScannerParam(searchParams); - if (scannerParam) { - setSelectedScanner(scannerParam); - } - }, [searchParams, setSelectedScanner]); - return ( -
- - - {error && } - {!error && selectedScan && ( - <> - - - - - - )} -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/ScanPanelBody.module.css b/src/inspect_scout/_view/www/src/app/scan/ScanPanelBody.module.css deleted file mode 100644 index a2c32bf24..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/ScanPanelBody.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.tabSet { - overflow-y: hidden; -} - -.tabs { - padding: 0.5em !important; - border-bottom: solid 1px var(--bs-border-color); -} - -.tabControl { - padding: 0.3em 1em !important; -} diff --git a/src/inspect_scout/_view/www/src/app/scan/ScanPanelBody.tsx b/src/inspect_scout/_view/www/src/app/scan/ScanPanelBody.tsx deleted file mode 100644 index 04b8a0e36..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/ScanPanelBody.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import clsx from "clsx"; -import { ReactNode, useEffect, useMemo, useRef, useState } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { GRID_STATE_NAME } from "../../components/DataframeView"; -import { ApplicationIcons } from "../../components/icons"; -import JSONPanel from "../../components/JsonPanel"; -import { SegmentedControl } from "../../components/SegmentedControl"; -import { TabPanel, TabSet } from "../../components/TabSet"; -import { useStore } from "../../state/store"; -import { Status } from "../../types/api-types"; -import { ResultGroup } from "../types"; -import { resultIdentifierStr, resultLog } from "../utils/results"; - -import { ScanInfo } from "./info/ScanInfo"; -import { DataframeGridApiProvider } from "./scanners/dataframe/DataframeGridApiContext"; -import { ScannerDataframeClearFiltersButton } from "./scanners/dataframe/ScannerDataframeClearFiltersButton"; -import { ScannerDataframeColumnsPopover } from "./scanners/dataframe/ScannerDataframeColumnsPopover"; -import { - ScannerDataframeCopyCSVButton, - ScannerDataframeDownloadCSVButton, -} from "./scanners/dataframe/ScannerDataframeCSVButtons"; -import { ScannerDataframeFilterColumnsButton } from "./scanners/dataframe/ScannerDataframeFilterColumnsButton"; -import { ScannerDataframeWrapTextButton } from "./scanners/dataframe/ScannerDataframeWrapTextButton"; -import { ScannerResultsFilter } from "./scanners/results/ScannerResultsFilter"; -import { ScannerResultsGroup } from "./scanners/results/ScannerResultsGroup"; -import { ScannerResultsSearch } from "./scanners/results/ScannerResultsSearch"; -import { ScannerPanel } from "./scanners/ScannerPanel"; -import styles from "./ScanPanelBody.module.css"; - -const kTabIdScans = "scan-detail-tabs-results"; -const kTabIdInfo = "scan-detail-tabs-info"; -const kTabIdJson = "scan-detail-tabs-json"; - -export const kSegmentList = "list"; -export const kSegmentDataframe = "dataframe"; - -export const ScanPanelBody: React.FC<{ selectedScan: Status }> = ({ - selectedScan, -}) => { - const [searchParams, setSearchParams] = useSearchParams(); - const selectedTab = useStore((state) => state.selectedResultsTab); - const setSelectedResultsTab = useStore( - (state) => state.setSelectedResultsTab - ); - - const selectedScanner = useStore((state) => state.selectedScanner); - const visibleScannerResults = useStore( - (state) => state.visibleScannerResults - ); - - const chooseColumnnsRef = useRef(null); - const [buttonElement, setButtonElement] = useState( - null - ); - - const gridFilter = useStore( - (state) => state.gridStates[GRID_STATE_NAME]?.filter - ); - - // Use a callback ref to capture the button element and trigger re-renders - const buttonRefCallback = (element: HTMLButtonElement | null) => { - chooseColumnnsRef.current = element; - setButtonElement(element); - }; - - // Sync URL tab parameter with store on mount and URL changes - useEffect(() => { - const tabParam = searchParams.get("tab"); - if (tabParam) { - // Valid tab IDs - const validTabs = [kTabIdScans, kTabIdInfo, kTabIdJson]; - if (validTabs.includes(tabParam)) { - setSelectedResultsTab(tabParam); - } - } - }, [searchParams, setSelectedResultsTab]); - - // Helper function to update both store and URL - const handleTabChange = (tabId: string) => { - setSelectedResultsTab(tabId); - setSearchParams({ tab: tabId }); - }; - - const selectedResultsView = - useStore((state) => state.selectedResultsView) || kSegmentList; - const setSelectedResultsView = useStore( - (state) => state.setSelectedResultsView - ); - - // Figure out whether grouping should be shown - const groupOptions: Array = useMemo(() => { - if (!visibleScannerResults || visibleScannerResults.length === 0) { - return []; - } - - const hasLabel = visibleScannerResults.some( - (summary) => summary.label !== undefined && summary.label !== null - ); - - const logCount = visibleScannerResults.reduce((logs, summary) => { - const log = resultLog(summary); - if (log) { - logs.add(log); - return logs; - } else { - return logs; - } - }, new Set()).size; - const hasManyLogs = logCount > 1; - - const idStrs = visibleScannerResults - .map((summary) => resultIdentifierStr(summary)) - .filter((id): id is string => id !== undefined); - const hasRepeatedIds = idStrs.length !== new Set(idStrs).size; - - const epochStrs = visibleScannerResults - .map((summary) => summary.transcriptMetadata.epoch) - .filter((e): e is number => e !== undefined); - const hasEpochs = new Set(epochStrs).size > 1; - - const models = visibleScannerResults - .map((summary) => summary.transcriptModel) - .filter((m) => m !== undefined); - const hasModels = models.length > 1; - - const options: Array = []; - if (hasLabel) { - options.push("label"); - } - if (hasManyLogs) { - options.push("source"); - } - if (hasRepeatedIds) { - options.push("id"); - } - if (hasEpochs) { - options.push("epoch"); - } - if (hasModels) { - options.push("model"); - } - return options; - // TODO: lint react-hooks/exhaustive-deps - refactor to avoid the lint - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedScanner, visibleScannerResults]); - - const tools: ReactNode[] = []; - if (selectedTab === kTabIdScans || selectedTab === undefined) { - if (selectedResultsView === kSegmentList) { - tools.push(); - } - - if (selectedResultsView === kSegmentList) { - tools.push(); - } - - if (selectedResultsView === kSegmentDataframe) { - tools.push( - - ); - tools.push( - - ); - tools.push( - - ); - } - - if (selectedResultsView === kSegmentDataframe) { - tools.push( - - ); - } - - if (selectedResultsView === kSegmentDataframe && gridFilter) { - tools.push(); - } - - if (selectedResultsView === kSegmentList && groupOptions.length > 0) { - tools.push( - - ); - } - - tools.push( - { - setSelectedResultsView(segmentId); - }} - /> - ); - } - - return ( - - - { - handleTabChange(kTabIdScans); - }} - > - - - - { - handleTabChange(kTabIdInfo); - }} - > - - - { - handleTabChange(kTabIdJson); - }} - scrollable={true} - > - - - - {selectedResultsView === kSegmentDataframe && buttonElement && ( - - )} - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/ScanPanelTitle.module.css b/src/inspect_scout/_view/www/src/app/scan/ScanPanelTitle.module.css deleted file mode 100644 index 72d2f21b1..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/ScanPanelTitle.module.css +++ /dev/null @@ -1,48 +0,0 @@ -.scanTitleView { - display: grid; - grid-template-columns: max-content max-content; - justify-content: space-between; - column-gap: 1em; - padding: 0.5em; -} - -.leftColumn { - display: grid; - grid-template-columns: max-content max-content 1fr; - align-items: baseline; - row-gap: 0.2rem; - column-gap: 0.3rem; -} - -.rightColumn { -} - -.scanTitleView h1 { - margin: 0; - font-weight: 600; - font-size: 1.3em; -} - -.scanTitleView h2 { - margin: 0; - font-weight: 500; - font-size: 1em; - color: var(--bs-secondary-text-color); -} - -.scanTitleView .secondaryRow { - display: grid; - grid-template-columns: max-content max-content; - column-gap: 0.2rem; -} - -.subtitle { - grid-column: 1 / -1; - display: grid; - grid-template-columns: max-content max-content max-content max-content max-content; - column-gap: 0.1rem; - margin: 0; - font-size: 0.8em; - font-weight: 400; - color: var(--bs-secondary-text-color); -} diff --git a/src/inspect_scout/_view/www/src/app/scan/ScanPanelTitle.tsx b/src/inspect_scout/_view/www/src/app/scan/ScanPanelTitle.tsx deleted file mode 100644 index e6d4d0ff1..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/ScanPanelTitle.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { CopyButton } from "../../components/CopyButton"; -import { ApplicationIcons } from "../../components/icons"; -import { Status } from "../../types/api-types"; -import { formatDateTime } from "../../utils/format"; -import { toRelativePath } from "../../utils/path"; -import { prettyDirUri } from "../../utils/uri"; - -import styles from "./ScanPanelTitle.module.css"; - -export const ScanPanelTitle: FC<{ - resultsDir: string | undefined; - selectedScan: Status; -}> = ({ resultsDir, selectedScan }) => { - const scanJobName = - selectedScan.spec.scan_name === "job" - ? "scan" - : selectedScan.spec.scan_name; - - const scannerModel = selectedScan.spec.model?.model; - - // Awesome - const deprecatedCount = selectedScan.spec.transcripts?.count || 0; - const modernCorrectCount = Object.keys( - selectedScan.spec.transcripts?.transcript_ids || {} - ).length; - const transcriptCount = Math.max(deprecatedCount, modernCorrectCount); - - return ( -
-
-

{scanJobName}:

-
-

- {toRelativePath(selectedScan.location, resultsDir)} - {scannerModel ? ` (${scannerModel})` : ""} -

- {selectedScan.location && ( - - )} -
-
-
- -
-
{transcriptCount} Transcripts
-
-
- {selectedScan.spec.timestamp - ? formatDateTime(new Date(selectedScan.spec.timestamp)) - : ""} -
-
-
- -
-
- ); -}; - -const StatusDisplay: FC<{ status?: Status }> = ({ status }) => { - const errorCount = status?.errors.length || 0; - - if (errorCount > 0) { - const errorStr = - errorCount === 1 - ? `${errorCount} Error` - : errorCount > 1 - ? `${errorCount} Errors` - : ""; - - return ( -
- {errorStr} -
- ); - } - - const statusStr = - status === undefined ? "" : status.complete ? "Complete" : "Incomplete"; - const statusIcon = - status === undefined - ? ApplicationIcons.running - : status.complete - ? ApplicationIcons.successSubtle - : ApplicationIcons.pendingTaskSubtle; - - return ( -
- {statusStr} -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/info/ScanInfo.module.css b/src/inspect_scout/_view/www/src/app/scan/info/ScanInfo.module.css deleted file mode 100644 index edf30fabe..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/info/ScanInfo.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.container { - margin: 0.5em; -} diff --git a/src/inspect_scout/_view/www/src/app/scan/info/ScanInfo.tsx b/src/inspect_scout/_view/www/src/app/scan/info/ScanInfo.tsx deleted file mode 100644 index 05e99c38c..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/info/ScanInfo.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { Card, CardBody, CardHeader } from "../../../components/Card"; -import { MetaDataGrid } from "../../../components/content/MetaDataGrid"; -import { RecordTree } from "../../../components/content/RecordTree"; -import { Status } from "../../../types/api-types"; - -import styles from "./ScanInfo.module.css"; - -export const ScanInfo: FC<{ selectedScan: Status }> = ({ selectedScan }) => { - return ( - <> - - - - - ); -}; - -interface ScanInfoCardProps { - selectedScan: Status; - className?: string | string[]; -} -const ScanInfoCard: FC = ({ selectedScan, className }) => { - const record = { - ID: selectedScan.spec.scan_id, - Name: selectedScan.spec.scan_name, - }; - if (selectedScan.spec.scan_args) { - record["Args"] = selectedScan.spec.scan_args; - } - if (selectedScan.spec.scan_file) { - record["Source File"] = selectedScan.spec.scan_file; - } - if (selectedScan.spec.revision?.origin) { - record["Origin"] = selectedScan.spec.revision?.origin; - } - if (selectedScan.spec.revision?.commit) { - record["Commit"] = selectedScan.spec.revision?.commit; - } - if (selectedScan.spec.packages) { - record["Packages"] = selectedScan.spec.packages; - } - if (selectedScan.spec.options) { - record["Options"] = selectedScan.spec.options; - } - - return ( - - - - ); -}; - -interface ScanMetadataCardProps { - selectedScan: Status; - className?: string | string[]; -} -const ScanMetadataCard: FC = ({ - selectedScan, - className, -}) => { - if ( - !selectedScan.spec.metadata || - Object.keys(selectedScan.spec.metadata).length === 0 - ) { - return null; - } - - return ( - - - - ); -}; - -interface ScannerInfoCardProps { - selectedScan: Status; - className?: string | string[]; -} - -const ScannerInfoCard: FC = ({ - selectedScan, - className, -}) => { - return ( - - - - ); -}; - -interface InfoCardProps { - title: string; - className?: string | string[]; - children?: React.ReactNode; -} - -const InfoCard: FC = ({ title, className, children }) => { - return ( - - - {children} - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerPanel.module.css b/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerPanel.module.css deleted file mode 100644 index 3922113d1..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerPanel.module.css +++ /dev/null @@ -1,17 +0,0 @@ -.root { - display: grid; - grid-template-rows: max-content 1fr max-content; - height: 100%; - width: 100%; -} - -.container { - display: grid; - grid-template-columns: 200px 1fr; - height: 100%; - width: 100%; -} - -.breadcrumbs { - min-width: 600px !important; -} diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerPanel.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerPanel.tsx deleted file mode 100644 index 952276d88..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerPanel.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { LoadingBar } from "../../../components/LoadingBar"; -import { useStore } from "../../../state/store"; -import { Status } from "../../../types/api-types"; -import { Footer } from "../../components/Footer"; -import { useSelectedScanDataframe } from "../../hooks/useSelectedScanDataframe"; -import { useSelectedScanner } from "../../hooks/useSelectedScanner"; - -import { ScannerResultsBody } from "./results/ScannerResultsBody"; -import styles from "./ScannerPanel.module.css"; -import { ScannerSidebar } from "./ScannerSidebar"; - -export const ScannerPanel: FC<{ selectedScan: Status }> = ({ - selectedScan, -}) => { - const visibleItemsCount = useStore( - (state) => state.visibleScannerResultsCount - ); - const selectedScanner = useSelectedScanner(); - - const { - data: columnTable, - loading: isLoading, - error, - } = useSelectedScanDataframe(); - const selectedScannerInfo = { - columnTable, - isLoading, - error: error?.message, - }; - - return ( -
- -
- - -
-
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerSidebar.module.css b/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerSidebar.module.css deleted file mode 100644 index 7f1c49aad..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerSidebar.module.css +++ /dev/null @@ -1,54 +0,0 @@ -.container { - border-right: solid 1px var(--bs-border-color); -} - -.entry { - padding: 0.5em; - display: grid; - grid-template-columns: auto 1fr; - column-gap: 0.5rem; -} - -.entry.selected { - background-color: var(--bs-secondary-bg); -} - -.titleBlock { - margin-bottom: 0.1rem; - grid-column: 1 / -1; -} - -.validations { - margin-top: 0.3rem; - grid-column: 1 / -1; -} - -.subTitle { - margin-top: -0.2rem; - max-height: 2rem; - overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 2; /* adjust based on your line-height */ - -webkit-box-orient: vertical; - word-wrap: break-word; -} - -.selected .title { - font-weight: 600; -} - -.entry:hover { - color: var(--bs-link-hover-color); - cursor: pointer; -} - -.numericResultTable { - display: grid; - grid-template-columns: auto auto; - column-gap: 0.5rem; - row-gap: 0; -} - -.contents { - display: contents; -} diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerSidebar.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerSidebar.tsx deleted file mode 100644 index 18d8bda61..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/ScannerSidebar.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import clsx from "clsx"; -import { FC, Fragment, useCallback, useRef } from "react"; -import { useSearchParams } from "react-router-dom"; -import { VirtuosoHandle } from "react-virtuoso"; - -import { ApplicationIcons } from "../../../components/icons"; -import { LabeledValue } from "../../../components/LabeledValue"; -import { LiveVirtualList } from "../../../components/LiveVirtualList"; -import { updateScannerParam } from "../../../router/url"; -import { useStore } from "../../../state/store"; -import { Status, ValidationResults } from "../../../types/api-types"; -import { formatPercent, formatPrettyDecimal } from "../../../utils/format"; -import { useSelectedScanner } from "../../hooks/useSelectedScanner"; - -import styles from "./ScannerSidebar.module.css"; - -export const ScannerSidebar: FC<{ selectedScan: Status }> = ({ - selectedScan, -}) => { - const entries = toEntries(selectedScan); - - const scanListHandle = useRef(null); - const renderRow = useCallback( - (index: number, entry: ScanResultsOutlineEntry) => { - return ; - }, - [] - ); - - return ( -
- - id={"scans-toc-list"} - listHandle={scanListHandle} - data={entries} - renderRow={renderRow} - /> -
- ); -}; - -const ScanResultsRow: FC<{ index: number; entry: ScanResultsOutlineEntry }> = ({ - index, - entry, -}) => { - const { data: selectedScanner } = useSelectedScanner(); - const setSelectedScanner = useStore((state) => state.setSelectedScanner); - const [searchParams, setSearchParams] = useSearchParams(); - const handleClick = useCallback( - (title: string) => { - setSelectedScanner(title); - setSearchParams(updateScannerParam(searchParams, title), { - replace: true, - }); - }, - [setSelectedScanner, searchParams, setSearchParams] - ); - - return ( -
{ - handleClick(entry.title); - }} - > -
-
- {entry.title} -
- {entry.params && entry.params.length > 0 && ( -
- {entry.params.join("")} -
- )} -
- - - {entry.results} - - - {Object.keys(entry.metrics).map((key) => { - return ( - - {entry.metrics[key] !== undefined - ? formatPrettyDecimal(entry.metrics[key]) - : "n/a"} - - ); - })} - - {!!entry.errors && ( - - {entry.errors} - - )} - - {entry.validations !== undefined && ( - - formatPercent(value, 1)} - /> - - )} -
- ); -}; - -interface ScanResultsOutlineEntry { - icon?: string; - title: string; - tokens?: number; - results: number; - scans: number; - validations?: Record; - errors?: number; - params?: string[]; - metrics: Record; -} - -const toEntries = (status?: Status): ScanResultsOutlineEntry[] => { - if (!status) { - return []; - } - const entries: ScanResultsOutlineEntry[] = []; - const scanners = status.summary.scanners || {}; - for (const scanner of Object.keys(scanners)) { - // The summary - const summary = scanners[scanner]; - - // The configuration - const scanInfo = status.spec.scanners[scanner]; - - const formattedParams: string[] = []; - if (scanInfo) { - const params = scanInfo.params || {}; - for (const [key, value] of Object.entries(params)) { - formattedParams.push(`${key}=${JSON.stringify(value)}`); - } - } - - const validations = resolveValidations(summary?.validation); - - const metrics = - summary && - summary.metrics && - Object.keys(summary.metrics).includes(scanner) - ? summary.metrics[scanner]! - : {}; - - entries.push({ - icon: ApplicationIcons.scorer, - title: scanner, - results: summary?.results || 0, - scans: summary?.scans || 0, - tokens: summary?.tokens, - errors: summary?.errors, - params: formattedParams, - validations: validations, - metrics, - }); - } - return entries; -}; - -/** - * Extract validation metrics from pre-computed ValidationResults. - */ -const resolveValidations = ( - validation: ValidationResults | undefined | null -): Record | undefined => { - if (!validation?.metrics) { - return undefined; - } - - const m = validation.metrics; - const result: Record = {}; - - // Add metrics in display order: accuracy, precision, recall, f1 - if (m.accuracy !== null && m.accuracy !== undefined) { - result["accuracy"] = m.accuracy; - } - if (m.precision !== null && m.precision !== undefined) { - result["precision"] = m.precision; - } - if (m.recall !== null && m.recall !== undefined) { - result["recall"] = m.recall; - } - if (m.f1 !== null && m.f1 !== undefined) { - result["f1"] = m.f1; - } - - // Return undefined if we couldn't extract any metrics - if (Object.keys(result).length === 0) { - return undefined; - } - - return result; -}; - -const NumericResultsTable: FC<{ - results: Record; - maxrows?: number; - formatter?: (value: number) => string; -}> = ({ results: validations, formatter }) => { - return ( -
- {Object.entries(validations).map(([key, value]) => ( - -
{key}
-
- {formatter ? formatter(value) : value} -
-
- ))} -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/DataframeGridApiContext.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/DataframeGridApiContext.tsx deleted file mode 100644 index b3dc64ee1..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/DataframeGridApiContext.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { GridApi } from "ag-grid-community"; -import { - createContext, - FC, - ReactNode, - useCallback, - useContext, - useState, -} from "react"; - -interface DataframeGridApiContextValue { - gridApi: GridApi | null; - setGridApi: (api: GridApi | null) => void; -} - -const DataframeGridApiContext = - createContext(null); - -export const DataframeGridApiProvider: FC<{ children: ReactNode }> = ({ - children, -}) => { - const [gridApi, setGridApiState] = useState(null); - - const setGridApi = useCallback((api: GridApi | null) => { - setGridApiState(api); - }, []); - - return ( - - {children} - - ); -}; - -export const useDataframeGridApi = (): GridApi | null => { - const context = useContext(DataframeGridApiContext); - if (!context) { - // Return null if not within provider - buttons will be disabled - return null; - } - return context.gridApi; -}; - -export const useSetDataframeGridApi = (): ((api: GridApi | null) => void) => { - const context = useContext(DataframeGridApiContext); - if (!context) { - // Return no-op if not within provider - return () => {}; - } - return context.setGridApi; -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeCSVButtons.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeCSVButtons.tsx deleted file mode 100644 index 1e22cd241..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeCSVButtons.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { FC, useCallback, useEffect, useRef, useState } from "react"; - -import { ApplicationIcons } from "../../../../components/icons"; -import { ToolButton } from "../../../../components/ToolButton"; -import { useStore } from "../../../../state/store"; -import { defaultColumns } from "../types"; - -import { useDataframeGridApi } from "./DataframeGridApiContext"; - -/** Transient status for copy/download operations */ -type OperationStatus = "idle" | "success" | "error" | "empty"; - -/** Duration to show success/error feedback before resetting to idle */ -const FEEDBACK_DURATION_MS = 2000; - -/** - * Sanitize a string for use as a filename by replacing invalid characters. - * Returns fallback if result would be empty. - */ -const sanitizeFilename = (name: string, fallback = "scan"): string => { - const sanitized = name.replace(/[/\\<>:"|?*]/g, "_"); - return sanitized || fallback; -}; - -/** - * Generate a timestamp string suitable for filenames (e.g., "20240116T120000"). - */ -const getFileTimestamp = (): string => - new Date().toISOString().slice(0, 19).replace(/[:-]/g, ""); - -/** - * Hook for managing transient operation status with auto-reset. - * Handles cleanup on unmount to prevent state updates after unmount. - */ -const useOperationStatus = () => { - const [status, setStatus] = useState("idle"); - const timeoutRef = useRef(null); - const isMountedRef = useRef(true); - - // Track mounted state and cleanup timeout on unmount - useEffect(() => { - isMountedRef.current = true; - return () => { - isMountedRef.current = false; - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); - - const setTransientStatus = useCallback((newStatus: OperationStatus) => { - if (!isMountedRef.current) return; - - setStatus(newStatus); - - // Clear any pending timeout - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - - // Auto-reset to idle after feedback duration (except for idle itself) - if (newStatus !== "idle") { - timeoutRef.current = window.setTimeout(() => { - if (isMountedRef.current) { - setStatus("idle"); - } - }, FEEDBACK_DURATION_MS); - } - }, []); - - return [status, setTransientStatus] as const; -}; - -/** - * Button to copy filtered dataframe data as CSV to clipboard. - * Exports only the currently visible columns from the column selector. - */ -export const ScannerDataframeCopyCSVButton: FC = () => { - const gridApi = useDataframeGridApi(); - const visibleColumns = useStore((state) => state.dataframeFilterColumns); - const [status, setStatus] = useOperationStatus(); - - const handleCopy = useCallback(() => { - if (!gridApi) return; - - // Check clipboard API availability (not available in non-secure contexts) - if (!navigator.clipboard) { - console.error("Clipboard API not available (requires HTTPS)"); - setStatus("error"); - return; - } - - // Use visible columns from column selector, falling back to defaults - const columnKeys = visibleColumns ?? defaultColumns; - - let csvData: string | undefined; - try { - csvData = gridApi.getDataAsCsv({ columnKeys }); - } catch (e: unknown) { - console.error("Failed to get CSV data from grid:", e); - setStatus("error"); - return; - } - - if (!csvData) { - // No data to copy (empty or all filtered out) - setStatus("empty"); - return; - } - - navigator.clipboard.writeText(csvData).then( - () => setStatus("success"), - (err: unknown) => { - console.error("Failed to copy CSV to clipboard:", err); - setStatus("error"); - } - ); - }, [gridApi, visibleColumns, setStatus]); - - const icon = - status === "error" || status === "empty" - ? ApplicationIcons.error - : status === "success" - ? ApplicationIcons.check - : ApplicationIcons.copy; - - const label = - status === "error" - ? "Failed" - : status === "empty" - ? "No data" - : status === "success" - ? "Copied" - : "Copy CSV"; - - return ( - - ); -}; - -/** - * Button to download filtered dataframe data as a CSV file. - * Exports only the currently visible columns from the column selector. - */ -export const ScannerDataframeDownloadCSVButton: FC = () => { - const gridApi = useDataframeGridApi(); - const selectedScanner = useStore((state) => state.selectedScanner); - const visibleColumns = useStore((state) => state.dataframeFilterColumns); - const [status, setStatus] = useOperationStatus(); - - const handleDownload = useCallback(() => { - if (!gridApi) return; - - try { - const timestamp = getFileTimestamp(); - const scannerName = sanitizeFilename(selectedScanner ?? "scan"); - const fileName = `${scannerName}_${timestamp}.csv`; - - // Use visible columns from column selector, falling back to defaults - const columnKeys = visibleColumns ?? defaultColumns; - - gridApi.exportDataAsCsv({ fileName, columnKeys }); - setStatus("success"); - } catch (e: unknown) { - console.error("Failed to export CSV:", e); - setStatus("error"); - } - }, [gridApi, selectedScanner, visibleColumns, setStatus]); - - const icon = - status === "error" - ? ApplicationIcons.error - : status === "success" - ? ApplicationIcons.check - : ApplicationIcons.download; - - const label = - status === "error" - ? "Failed" - : status === "success" - ? "Downloaded" - : "Download CSV"; - - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeClearFiltersButton.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeClearFiltersButton.tsx deleted file mode 100644 index bfd9bb547..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeClearFiltersButton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { FC, useCallback } from "react"; - -import { GRID_STATE_NAME } from "../../../../components/DataframeView"; -import { ApplicationIcons } from "../../../../components/icons"; -import { ToolButton } from "../../../../components/ToolButton"; -import { useStore } from "../../../../state/store"; - -export const ScannerDataframeClearFiltersButton: FC = () => { - const setGridState = useStore((state) => state.setGridState); - const gridState = useStore((state) => state.gridStates[GRID_STATE_NAME]); - - const clearState = useCallback(() => { - const { filter, ...state } = gridState || {}; - setGridState(GRID_STATE_NAME, state); - }, [gridState, setGridState]); - - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeColumnsPopover.module.css b/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeColumnsPopover.module.css deleted file mode 100644 index 4fb517864..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeColumnsPopover.module.css +++ /dev/null @@ -1,42 +0,0 @@ -.grid { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - column-gap: 2em; - row-gap: 0.15em; -} - -.row { - display: flex; - align-items: center; - gap: 0.5em; - cursor: pointer; - border-radius: var(--bs-border-radius); - padding: 0.1em 0.4em; - margin: 0 -0.4em; -} - -.row:hover { - background-color: var(--bs-secondary-bg); -} - -.links { - display: flex; - padding-bottom: 0.2em; - margin-bottom: 0.4em; - column-gap: 0.3em; - border-bottom: solid 1px var(--bs-border-color); -} - -.links a { - cursor: pointer; - text-decoration: none; - color: var(--bs-link-color); -} - -.links a:hover { - color: var(--bs-link-hover-color); -} - -.selected { - font-weight: 600; -} diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeColumnsPopover.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeColumnsPopover.tsx deleted file mode 100644 index 4ad6dba89..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeColumnsPopover.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import clsx from "clsx"; -import { FC, useCallback } from "react"; - -import { PopOver } from "../../../../components/PopOver"; -import { useStore } from "../../../../state/store"; - -import { defaultColumns } from "./../types"; -import styles from "./ScannerDataframeColumnsPopover.module.css"; - -export interface ScannerDataframeColumnsPopoverProps { - positionEl: HTMLElement | null; -} - -const columnsGroups = { - Transcript: [ - "transcript_id", - "transcript_source_type", - "transcript_source_id", - "transcript_source_uri", - "transcript_metadata", - "transcript_date", - "transcript_task_set", - "transcript_task_id", - "transcript_task_repeat", - "transcript_agent", - "transcript_agent_args", - "transcript_model", - "transcript_score", - "transcript_success", - "transcript_message_count", - "transcript_total_time", - "transcript_total_tokens", - "transcript_error", - "transcript_limit", - ], - Scan: [ - "scan_id", - "scan_tags", - "scan_metadata", - "scan_git_origin", - "scan_git_version", - "scan_git_commit", - ], - Scanner: [ - "scanner_key", - "scanner_name", - "scanner_version", - "scanner_package_version", - "scanner_file", - "scanner_params", - ], - Input: ["input_type", "input_ids"], - Validation: ["validation_target", "validation_result"], - Result: [ - "uuid", - "value", - "explanation", - "metadata", - "label", - "value_type", - "answer", - "scan_tokens_total", - "scan_model_usage", - "scan_events", - "timestamp", - "message_references", - "event_references", - ], - Error: ["scan_error", "scan_error_traceback", "scan_error_type"], -}; - -const useDataframeColumns = () => { - const allColumns: string[] = Object.values(columnsGroups).flat(); - - const filteredColumns = - useStore((state) => state.dataframeFilterColumns) || defaultColumns; - const setFilteredColumns = useStore( - (state) => state.setDataframeFilterColumns - ); - const isDefaultFilter = - filteredColumns?.length === defaultColumns.length && - filteredColumns.every((col) => defaultColumns.includes(col)); - const isAllFilter = filteredColumns?.length === allColumns.length; - const setDefaultFilter = () => { - setFilteredColumns(defaultColumns); - }; - const setAllFilter = () => { - setFilteredColumns(allColumns); - }; - const filterColumn = useCallback( - (column: string, show: boolean) => { - if (show && !filteredColumns?.includes(column)) { - setFilteredColumns([...(filteredColumns || []), column]); - } else if (!show) { - setFilteredColumns(filteredColumns?.filter((c) => c !== column) || []); - } - }, - [filteredColumns, setFilteredColumns] - ); - - const arrangedColumns = (cols: number): Record[] => { - // Returns an array of records, one for each column of checkboxes - // Each record maps group names to arrays of columns in that group - - // Define the desired order of groups with "---" as column break separator - const groupOrder = [ - "Result", - "Input", - "---", - "Transcript", - "Validation", - "Error", - "---", - "Scan", - "Scanner", - ]; - - // Group all available columns by their group - const groupedColumns: Record = {}; - - Object.entries(columnsGroups).forEach(([groupName, columns]) => { - const columnsInGroup = columns.filter((col) => { - // Handle wildcard patterns like "validation_result_*" - if (col.endsWith("*")) { - const prefix = col.slice(0, -1); - return allColumns.some((c) => c.startsWith(prefix)); - } - return allColumns.includes(col); - }); - - if (columnsInGroup.length > 0) { - groupedColumns[groupName] = columnsInGroup; - } - }); - - // Split groupOrder by separator and distribute into columns - const result: Record[] = []; - let currentColumn: Record = {}; - - groupOrder.forEach((item) => { - if (item === "---") { - // Start a new column - if (Object.keys(currentColumn).length > 0) { - result.push(currentColumn); - currentColumn = {}; - } - } else if (groupedColumns[item]) { - // Add group to current column - currentColumn[item] = groupedColumns[item]; - } - }); - - // Add the last column if it has content - if (Object.keys(currentColumn).length > 0) { - result.push(currentColumn); - } - - // Pad with empty columns if needed to match requested column count - while (result.length < cols) { - result.push({}); - } - - return result; - }; - - return { - defaultFilter: defaultColumns, - isDefaultFilter, - isAllFilter, - setDefaultFilter, - setAllFilter, - filterColumn, - filtered: filteredColumns || [], - arrangedColumns, - }; -}; - -export const ScannerDataframeColumnsPopover: FC< - ScannerDataframeColumnsPopoverProps -> = ({ positionEl }) => { - const showFilter = useStore((state) => state.dataframeShowFilterColumns); - const setShowFilter = useStore( - (state) => state.setDataframeShowFilterColumns - ); - - const { - isDefaultFilter, - isAllFilter, - setDefaultFilter, - setAllFilter, - filterColumn, - filtered, - arrangedColumns, - } = useDataframeColumns(); - return ( - - - -
- {arrangedColumns(3).map((columnGroup, colIndex) => { - return ( -
- {Object.entries(columnGroup).map(([groupName, columns]) => ( -
-
- {groupName} -
- {columns.map((column) => ( -
{ - filterColumn(column, !filtered.includes(column)); - }} - > - { - filterColumn(column, e.target.checked); - }} - > - {column} -
- ))} -
- ))} -
- ); - })} -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeFilterColumnsButton.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeFilterColumnsButton.tsx deleted file mode 100644 index fadff6142..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeFilterColumnsButton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { forwardRef, useCallback } from "react"; - -import { ApplicationIcons } from "../../../../components/icons"; -import { ToolButton } from "../../../../components/ToolButton"; -import { useStore } from "../../../../state/store"; - -export const ScannerDataframeFilterColumnsButton = forwardRef< - HTMLButtonElement, - unknown ->((_, ref) => { - const showFilter = useStore((state) => state.dataframeShowFilterColumns); - const setShowFilter = useStore( - (state) => state.setDataframeShowFilterColumns - ); - - const toggleShowFilter = useCallback(() => { - setShowFilter(!showFilter); - }, [showFilter, setShowFilter]); - - return ( - - ); -}); - -ScannerDataframeFilterColumnsButton.displayName = - "ScannerDataframeFilterColumnsButton"; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeWrapTextButton.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeWrapTextButton.tsx deleted file mode 100644 index 56051d311..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/dataframe/ScannerDataframeWrapTextButton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { FC, useCallback } from "react"; - -import { ApplicationIcons } from "../../../../components/icons"; -import { ToolButton } from "../../../../components/ToolButton"; -import { useStore } from "../../../../state/store"; - -export const ScannerDataframeWrapTextButton: FC = () => { - const wrapText = useStore((state) => state.dataframeWrapText); - const setWrapText = useStore((state) => state.setDataframeWrapText); - - const toggleWrapText = useCallback(() => { - setWrapText(!wrapText); - }, [wrapText, setWrapText]); - - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsGroup.module.css b/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsGroup.module.css deleted file mode 100644 index 03b0db019..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsGroup.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.row { - border-bottom: solid 1px var(--bs-border-color); - padding: 0.25rem 1rem; - background-color: rgba(var(--bs-secondary-rgb), 0.03); -} diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsGroup.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsGroup.tsx deleted file mode 100644 index 714fcafc5..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsGroup.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import styles from "./ScannerResultsGroup.module.css"; - -interface ScannerResultsGroupProps { - group: string; -} - -export const ScannerResultsGroup: FC = ({ - group, -}) => { - return ( -
-
- {group} -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsHeader.module.css b/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsHeader.module.css deleted file mode 100644 index cac9b6683..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsHeader.module.css +++ /dev/null @@ -1,21 +0,0 @@ -.header { - border-bottom: solid var(--bs-light-border-subtle) 1px; - padding: 0.3rem 1rem; - user-select: none; -} - -.center { - justify-self: center; -} - -.shrinkable { - min-width: 0; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.clickable { - cursor: pointer; -} diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsHeader.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsHeader.tsx deleted file mode 100644 index 8040231e4..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsHeader.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import clsx from "clsx"; -import { FC, useCallback, MouseEvent } from "react"; - -import { ApplicationIcons } from "../../../../components/icons"; -import { useStore } from "../../../../state/store"; - -import styles from "./ScannerResultsHeader.module.css"; -import { GridDescriptor } from "./ScannerResultsList"; - -interface ScannerResultsHeaderProps { - gridDescriptor: GridDescriptor; -} -export const ScannerResultsHeader: FC = ({ - gridDescriptor, -}) => { - // Column information - const hasExplanation = gridDescriptor.columns.includes("explanation"); - const hasLabel = gridDescriptor.columns.includes("label"); - const hasError = gridDescriptor.columns.includes("error"); - const hasValidations = gridDescriptor.columns.includes("validations"); - - return ( -
- - {hasLabel && } - - {hasValidations && } - {hasError && } -
- ); -}; - -const ColumnHeader: FC<{ - label: string; - className?: string | string[]; -}> = ({ label, className }) => { - const sortResults = useStore((state) => state.sortResults); - const setSortResults = useStore((state) => state.setSortResults); - const sort = sortResults?.find((s) => s.column === label); - - const handleSort = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - - const nextSortDirection = - sort?.direction === undefined - ? "asc" - : sort?.direction === "asc" - ? "desc" - : undefined; - - if (!nextSortDirection) { - // remove sorting for this column - if (e.shiftKey) { - // multi-column sort - const newSorts = sortResults - ? sortResults.filter((s) => s.column !== label) - : []; - setSortResults(newSorts.length > 0 ? newSorts : undefined); - } else { - setSortResults(undefined); - } - return; - } else { - if (e.shiftKey) { - // multi-column sort - const newSorts = sortResults ? [...sortResults] : []; - const existingIndex = newSorts.findIndex((s) => s.column === label); - if (existingIndex >= 0) { - newSorts[existingIndex] = { - column: label, - direction: nextSortDirection, - }; - } else { - newSorts.push({ column: label, direction: nextSortDirection }); - } - setSortResults(newSorts); - } else { - setSortResults([{ column: label, direction: nextSortDirection }]); - } - } - }, - // TODO: lint react-hooks/exhaustive-deps Fix this - // eslint-disable-next-line react-hooks/exhaustive-deps - [sort, sortResults, setSortResults] - ); - - return ( -
- {label} - {sort && ( - - )} -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsList.module.css b/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsList.module.css deleted file mode 100644 index 7303f2ab1..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsList.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.container { - height: 100%; - width: 100%; -} diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsList.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsList.tsx deleted file mode 100644 index 1eb045505..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsList.tsx +++ /dev/null @@ -1,599 +0,0 @@ -import { ColumnTable } from "arquero"; -import clsx from "clsx"; -import { FC, useCallback, useEffect, useMemo, useRef } from "react"; -import { useSearchParams } from "react-router-dom"; -import { VirtuosoHandle } from "react-virtuoso"; - -import { LiveVirtualList } from "../../../../components/LiveVirtualList"; -import { LoadingBar } from "../../../../components/LoadingBar"; -import { NoContentsPanel } from "../../../../components/NoContentsPanel"; -import { useLoggingNavigate } from "../../../../debugging/navigationDebugging"; -import { scanResultRoute } from "../../../../router/url"; -import { useStore } from "../../../../state/store"; -import { Status } from "../../../../types/api-types"; -import { basename } from "../../../../utils/path"; -import { useScanResultSummaries } from "../../../hooks/useScanResultSummaries"; -import { useScanRoute } from "../../../hooks/useScanRoute"; -import { ScanResultSummary, SortColumn } from "../../../types"; -import { - resultIdentifier, - resultIdentifierStr, - resultLog, -} from "../../../utils/results"; -import { - kFilterAllResults, - kFilterPositiveResults, -} from "../results/ScannerResultsFilter"; - -import { ScannerResultsGroup } from "./ScannerResultsGroup"; -import { ScannerResultsHeader } from "./ScannerResultsHeader"; -import styles from "./ScannerResultsList.module.css"; -import { ScannerResultsRow } from "./ScannerResultsRow"; - -export interface GridDescriptor { - gridStyle: Record; - columns: string[]; -} - -interface ResultGroup { - type: "group"; - label: string; -} - -// Type guard to check if entry is a ResultGroup -const isResultGroup = ( - entry: ResultGroup | ScanResultSummary -): entry is ResultGroup => { - return "type" in entry && entry.type === "group"; -}; - -interface ScannerResultsListProps { - id: string; - columnTable?: ColumnTable; - selectedScan: Status; -} -export const ScannerResultsList: FC = ({ - id, - columnTable, - selectedScan, -}) => { - // Url data - const navigate = useLoggingNavigate("ScannerResultsList"); - const [searchParams] = useSearchParams(); - const { scansDir, scanPath } = useScanRoute(); - - // Data - const { data: scannerSummaries, isLoading } = - useScanResultSummaries(columnTable); - - // Options / State - const listHandle = useRef(null); - const selectedScanResult = useStore((state) => state.selectedScanResult); - const selectedFilter = useStore((state) => state.selectedFilter); - const groupResultsBy = useStore((state) => state.groupResultsBy); - const scansSearchText = useStore((state) => state.scansSearchText); - - // Setters - const setVisibleScannerResults = useStore( - (state) => state.setVisibleScannerResults - ); - const setVisibleScannerResultsCount = useStore( - (state) => state.setVisibleScannerResultsCount - ); - const setSelectedScanResult = useStore( - (state) => state.setSelectedScanResult - ); - const setSelectedFilter = useStore((state) => state.setSelectedFilter); - - // Sorting - const sortResults = useStore((state) => state.sortResults); - const setSortResults = useStore((state) => state.setSortResults); - - useEffect(() => { - if (selectedFilter === undefined && selectedScan.complete === false) { - setSelectedFilter(kFilterAllResults); - } - }, [selectedScan, selectedFilter, setSelectedFilter]); - - // Apply text filtering to the scanner summaries - const filteredSummaries = useMemo(() => { - let textFiltered = scannerSummaries; - if (scansSearchText && scansSearchText.length > 0) { - const lowerSearch = scansSearchText.toLowerCase(); - textFiltered = scannerSummaries.filter((s) => { - const idStr = resultIdentifierStr(s) || ""; - const logStr = resultLog(s) || ""; - const labelStr = s.label || ""; - return ( - idStr.toLowerCase().includes(lowerSearch) || - logStr.toLowerCase().includes(lowerSearch) || - labelStr.toLowerCase().includes(lowerSearch) - ); - }); - } - - // Filter positives results if needed - const resultsFiltered = - selectedFilter === kFilterPositiveResults || selectedFilter === undefined - ? textFiltered.filter((s) => !!s.value) - : textFiltered; - - // Return filtered sorted summaries - if (sortResults === undefined || sortResults.length === 0) { - return resultsFiltered; - } else { - return [...resultsFiltered].sort((a, b) => - sortByColumns(a, b, sortResults) - ); - } - }, [scannerSummaries, selectedFilter, scansSearchText, sortResults]); - - // Set the default sort order when the filter changes (if there isn't an explicit order) - useEffect(() => { - if (filteredSummaries.length === 0 || sortResults) { - return; - } - - if ( - selectedFilter === kFilterAllResults && - filteredSummaries.some((s) => !!s.scanError) - ) { - // Default sort for error filter: errors first, then by identifier - setSortResults([{ column: "Error", direction: "desc" }]); - } else if ( - filteredSummaries.some((s) => s.validationResult !== undefined) - ) { - // Default sort for validations: validations first, then by identifier - setSortResults([{ column: "Validation", direction: "desc" }]); - } - }, [sortResults, selectedFilter, filteredSummaries, setSortResults]); - - // Compute the optimal column layout based on the current data - const gridDescriptor = useMemo(() => { - const descriptor = optimalColumnLayout(filteredSummaries); - return descriptor; - }, [filteredSummaries]); - - const rows: Array = useMemo(() => { - // No grouping - if (!groupResultsBy || groupResultsBy === "none") { - return filteredSummaries; - } - - const groups = new Map(); - - for (const item of filteredSummaries) { - // Insert group header when group changes - const groupKey = - groupResultsBy === "source" - ? basename(resultLog(item) || "") || "Unknown" - : groupResultsBy === "label" - ? item.label || "Unlabeled" - : groupResultsBy === "epoch" - ? (item.transcriptMetadata.epoch as number) - : groupResultsBy === "model" - ? item.transcriptModel || "Unknown" - : resultIdentifierStr(item) || "Unknown"; - - // Insert group header when group changes - if (!groups.has(groupKey)) { - groups.set(groupKey, []); - } - groups.get(groupKey)?.push(item); - } - - // Sort group keys (numerically if they're numbers, alphabetically otherwise) - const sortedGroupKeys = Array.from(groups.keys()).sort((a, b) => { - // If both are numbers, sort numerically - if (typeof a === "number" && typeof b === "number") { - return a - b; - } - // Otherwise, sort as strings - return String(a).localeCompare(String(b)); - }); - const result: Array = []; - - for (const groupKey of sortedGroupKeys) { - const label = - groupResultsBy === "epoch" ? `Epoch ${groupKey}` : String(groupKey); - result.push({ type: "group", label: label }); - result.push(...(groups.get(groupKey) || [])); - } - - return result; - }, [filteredSummaries, groupResultsBy]); - - const currentIndex = useMemo(() => { - if (selectedScanResult) { - return filteredSummaries.findIndex( - (s) => s.identifier === selectedScanResult - ); - } - return -1; - }, [selectedScanResult, filteredSummaries]); - - const handleNext = useCallback(() => { - if (currentIndex >= 0 && currentIndex < filteredSummaries.length - 1) { - const nextResult = filteredSummaries[currentIndex + 1]; - if (nextResult?.identifier) { - setSelectedScanResult(nextResult.identifier); - } - } - }, [currentIndex, filteredSummaries, setSelectedScanResult]); - - const handlePrevious = useCallback(() => { - if (currentIndex > 0) { - const previousResult = filteredSummaries[currentIndex - 1]; - if (previousResult?.identifier) { - setSelectedScanResult(previousResult.identifier); - } - } - }, [currentIndex, filteredSummaries, setSelectedScanResult]); - - const handleEnter = useCallback( - (newWindow?: boolean) => { - const selectedResult = filteredSummaries[currentIndex]; - if (!scansDir) { - return; - } - const route = scanResultRoute( - scansDir, - scanPath, - selectedResult?.identifier, - searchParams - ); - if (newWindow) { - window.open(route, "_blank"); - } else { - void navigate(route); - } - }, - [ - currentIndex, - filteredSummaries, - navigate, - scanPath, - searchParams, - scansDir, - ] - ); - - const hasPrevious = currentIndex > 0; - const hasNext = - currentIndex >= 0 && currentIndex < filteredSummaries.length - 1; - - // Global keydown handler for keyboard shortcuts - useEffect(() => { - const handleGlobalKeyDown = (e: globalThis.KeyboardEvent) => { - // Don't handle keyboard events if focus is on an input, textarea, or select element - const activeElement = document.activeElement; - const isInputFocused = - activeElement && - (activeElement.tagName === "INPUT" || - activeElement.tagName === "TEXTAREA" || - activeElement.tagName === "SELECT"); - - if (!isInputFocused) { - // Navigation shortcuts (only when not in an input field) - if (e.key === "ArrowUp") { - if (e.metaKey || e.ctrlKey) { - // Cmd/Ctrl+ArrowUp: Go to first item - if ( - filteredSummaries.length > 0 && - filteredSummaries[0]?.identifier - ) { - e.preventDefault(); - setSelectedScanResult(filteredSummaries[0].identifier); - } - } else if (hasPrevious) { - e.preventDefault(); - handlePrevious(); - } - } else if (e.key === "ArrowDown") { - if (e.metaKey || e.ctrlKey) { - // Cmd/Ctrl+ArrowDown: Go to last item - if (filteredSummaries.length > 0) { - e.preventDefault(); - const identifier = - filteredSummaries[filteredSummaries.length - 1]?.identifier; - if (identifier) { - setSelectedScanResult(identifier); - } - } - } else if (hasNext) { - e.preventDefault(); - handleNext(); - } - } else if (e.key === "Enter") { - e.preventDefault(); - handleEnter(e.metaKey || e.ctrlKey); - } - } - }; - - // Use capture phase to catch event before it reaches other handlers - document.addEventListener("keydown", handleGlobalKeyDown, true); - - return () => { - document.removeEventListener("keydown", handleGlobalKeyDown, true); - }; - }, [ - hasPrevious, - hasNext, - handlePrevious, - handleNext, - handleEnter, - filteredSummaries, - setSelectedScanResult, - ]); - - useEffect(() => { - // Only set if nothing is selected and we have results - if ( - !selectedScanResult && - filteredSummaries.length > 0 && - filteredSummaries[0]?.identifier - ) { - setSelectedScanResult(filteredSummaries[0].identifier); - } - }, [filteredSummaries, selectedScanResult, setSelectedScanResult]); - - useEffect(() => { - setVisibleScannerResults(filteredSummaries); - setVisibleScannerResultsCount(filteredSummaries.length); - }, [ - filteredSummaries, - setVisibleScannerResults, - setVisibleScannerResultsCount, - ]); - - const selectedItemIndex = useMemo(() => { - if (selectedScanResult) { - const selectedIndex = filteredSummaries.findIndex( - (s) => s.identifier === selectedScanResult - ); - if (selectedIndex >= 0) { - return selectedIndex; - } - } - return undefined; - }, [selectedScanResult, filteredSummaries]); - - useEffect(() => { - setTimeout(() => { - listHandle.current?.scrollToIndex({ - index: selectedItemIndex ?? 0, - align: "center", - behavior: "auto", - }); - }, 5); - }, [selectedItemIndex]); - - const renderRow = useCallback( - (index: number, entry: ScanResultSummary | ResultGroup) => { - if (isResultGroup(entry)) { - return ; - } - - return ( - - ); - }, - [gridDescriptor] - ); - - let noContentMessage: string | undefined = undefined; - if (!isLoading && scannerSummaries.length === 0) { - noContentMessage = "No scan results are available."; - } else if ( - !isLoading && - filteredSummaries.length === 0 && - selectedFilter !== kFilterAllResults && - !scansSearchText - ) { - noContentMessage = "No positive scan results were found."; - } else if (!isLoading && filteredSummaries.length === 0) { - noContentMessage = "No scan results match the current filter."; - } - - return ( -
- - - {noContentMessage && } - {!isLoading && filteredSummaries.length > 0 && ( - - id={id} - listHandle={listHandle} - data={rows} - renderRow={renderRow} - className={clsx(styles.list)} - animation={false} - /> - )} -
- ); -}; - -// Sorts scan results by multiple columns and directions. -// Applies sorting rules in order, falling back to the next rule if values are equal. -const sortByColumns = ( - a: ScanResultSummary, - b: ScanResultSummary, - sortColumns: SortColumn[] -): number => { - for (const sortCol of sortColumns) { - let comparison = 0; - - switch (sortCol.column.toLowerCase()) { - case "id": { - const identifierA = resultIdentifier(a); - const identifierB = resultIdentifier(b); - - if ( - typeof identifierA.id === "number" && - typeof identifierB.id === "number" - ) { - comparison = identifierA.id - identifierB.id; - } else { - comparison = String(identifierA.id).localeCompare( - String(identifierB.id) - ); - } - - if (comparison === 0 && identifierA.epoch && identifierB.epoch) { - comparison = identifierA.epoch - identifierB.epoch; - } - break; - } - case "explanation": { - const explA = a.explanation || ""; - const explB = b.explanation || ""; - comparison = explA.localeCompare(explB); - break; - } - case "label": { - const labelA = a.label || ""; - const labelB = b.label || ""; - comparison = labelA.localeCompare(labelB); - break; - } - case "value": { - const valueA = - a.value !== null && a.value !== undefined ? String(a.value) : ""; - const valueB = - b.value !== null && b.value !== undefined ? String(b.value) : ""; - comparison = valueA.localeCompare(valueB); - break; - } - case "error": { - const errorA = a.scanError || ""; - const errorB = b.scanError || ""; - comparison = errorA.localeCompare(errorB); - break; - } - case "validation": { - const validationA = a.validationResult ? 1 : 0; - const validationB = b.validationResult ? 1 : 0; - comparison = validationA - validationB; - break; - } - default: - // Unknown column, skip - continue; - } - - // Apply direction (asc or desc) - if (comparison !== 0) { - return sortCol.direction === "asc" ? comparison : -comparison; - } - } - - // All comparisons are equal - return 0; -}; - -const optimalColumnLayout = ( - scannerSummaries: ScanResultSummary[] -): GridDescriptor => { - const columns: string[] = []; - const gridColParts: string[] = []; - - // The explanation column, if any explanations exist - columns.push("result"); - gridColParts.push("10fr"); - - // The label column, if any labels exist - const hasLabel = scannerSummaries.some((s) => !!s.label); - if (hasLabel) { - columns.push("label"); - - const maxlabelLen = scannerSummaries.reduce((max, s) => { - return Math.max(max, s.label ? s.label.length : 0); - }, 0); - gridColParts.push( - `minmax(${Math.min(Math.max(maxlabelLen * 5, 75), 250)}px, 1fr)` - ); - } - - // The value column - columns.push("value"); - const hasValueObjs = scannerSummaries.some((s) => s.valueType === "object"); - if (hasValueObjs) { - const obj = scannerSummaries[0]?.value; - if (obj && typeof obj === "object" && !Array.isArray(obj)) { - // measure the length of the longest key - const maxKeyLen = Object.keys(obj).reduce((max, key) => { - return Math.max(max, key.length); - }, 0); - - // measure the length of the longest value - const maxValueLen = Object.values(obj).reduce((max, val) => { - const valStr = val !== undefined && val !== null ? String(val) : ""; - return Math.max(max, valStr.length); - }, 0); - - gridColParts.push( - `minmax(${Math.min(Math.max((maxKeyLen + maxValueLen) * 10, 135), 200)}px, 4fr)` - ); - } else { - gridColParts.push("3fr"); - } - } else { - const maxValueLen = scannerSummaries.reduce((max: number, s) => { - if (s.valueType === "array") { - const len = (s.value as unknown[]).reduce((prev, val) => { - const valStr = val !== undefined && val !== null ? String(val) : ""; - return Math.max(prev, valStr.length); - }, 0); - return Math.max(max, len); - } else { - const valStr = - s.value !== undefined && s.value !== null ? String(s.value) : ""; - return Math.max(max, valStr.length); - } - }, 0); - gridColParts.push( - `minmax(${Math.min(Math.max(maxValueLen * 4, 50), 300)}px, 1fr)` - ); - } - - const hasValidations = scannerSummaries.some( - (s) => s.validationResult !== undefined && s.validationResult !== null - ); - if (hasValidations) { - columns.push("validations"); - gridColParts.push("minmax(80px, 1fr)"); - } - - const hasErrors = scannerSummaries.some((s) => !!s.scanError); - if (hasErrors) { - const maxErrorLen = scannerSummaries.reduce((max, s) => { - return Math.max(max, s.scanError ? s.scanError.length : 0); - }, 0); - - columns.push("error"); - gridColParts.push( - `minmax(${Math.min(Math.max(maxErrorLen * 4, 50), 250)}px, 1fr)` - ); - } - - // Special case - if there is only an id and value column, divide space evenly - if (columns.length === 2 && columns[0] === "id" && columns[1] === "value") { - gridColParts[0] = "1fr"; - gridColParts[1] = "1fr"; - } - - return { - gridStyle: { - gridTemplateColumns: gridColParts.join(" "), - display: "grid", - columnGap: "1rem", - }, - columns, - }; -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsRow.module.css b/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsRow.module.css deleted file mode 100644 index 67eaa63fd..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsRow.module.css +++ /dev/null @@ -1,68 +0,0 @@ -.row { - border-bottom: solid 1px var(--bs-border-color); - padding: 0.5rem 1rem 1rem 1rem; - max-height: 200px; -} - -.row.noExplanation { - max-height: 100px; -} - -.row:hover { - cursor: pointer; -} - -.row.disabled { - opacity: 0.7; -} - -.row.disabled:hover { - cursor: default; -} - -.result { - display: grid; - grid-template-columns: max-content 1fr; - grid-template-rows: auto auto; - column-gap: 0.5rem; - row-gap: 0.5rem; - justify-content: space-between; -} - -.id { -} - -.model { - text-align: right; -} - -.label { - overflow-wrap: break-word; -} - -.explanation { - overflow: hidden; - text-overflow: ellipsis; - - grid-column: span 2; - max-height: calc(160px - 3rem); -} - -.row.selected { - background-color: rgba(var(--bs-secondary-rgb), 0.07); -} - -.link { - color: var(--bs-body-color); - text-decoration: none; -} - -.link:hover { - color: var(--bs-body-color); -} - -.error { - overflow: hidden; - text-overflow: ellipsis; - max-height: calc(160px - 3rem); -} diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsRow.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsRow.tsx deleted file mode 100644 index cd8e5ebe8..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/list/ScannerResultsRow.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import clsx from "clsx"; -import { FC, memo } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { MarkdownReference } from "../../../../components/MarkdownDivWithReferences"; -import { useLoggingNavigate } from "../../../../debugging/navigationDebugging"; -import { scanResultRoute } from "../../../../router/url"; -import { useStore } from "../../../../state/store"; -import { Error } from "../../../components/Error"; -import { Explanation } from "../../../components/Explanation"; -import { TaskName } from "../../../components/TaskName"; -import { ValidationResult } from "../../../components/ValidationResult"; -import { Value } from "../../../components/Value"; -import { useScanRoute } from "../../../hooks/useScanRoute"; -import { ScanResultSummary } from "../../../types"; -import { useMarkdownRefs } from "../../../utils/refs"; - -import { GridDescriptor } from "./ScannerResultsList"; -import styles from "./ScannerResultsRow.module.css"; - -interface ScannerResultsRowProps { - index: number; - summary: ScanResultSummary; - gridDescriptor: GridDescriptor; -} - -const ScannerResultsRowComponent: FC = ({ - summary, - gridDescriptor, -}) => { - // Path information - const { scansDir, scanPath } = useScanRoute(); - const [searchParams] = useSearchParams(); - - // selected scan result - const selectedScanResult = useStore((state) => state.selectedScanResult); - const setSelectedScanResult = useStore( - (state) => state.setSelectedScanResult - ); - - // Generate the route to the scan result using the current scan path and the entry's uuid - const isNavigable = summary.identifier !== undefined && !!scansDir; - const scanResultUrl = isNavigable - ? scanResultRoute(scansDir, scanPath, summary.identifier, searchParams) - : ""; - const navigate = useLoggingNavigate("ScannerResultsRow"); - - // Information about the row - const hasExplanation = gridDescriptor.columns.includes("result"); - const hasLabel = gridDescriptor.columns.includes("label"); - const hasErrors = gridDescriptor.columns.includes("error"); - const hasValidations = gridDescriptor.columns.includes("validations"); - - // refs - const refs: MarkdownReference[] = useMarkdownRefs(summary); - - // Task information - const taskSet = summary.transcriptTaskSet; - const taskId = summary.transcriptTaskId; - const taskRepeat = summary.transcriptTaskRepeat; - - const grid = ( -
{ - if (summary.identifier) { - setSelectedScanResult(summary.identifier); - } - }} - > - {hasExplanation && ( -
-
- -
- -
- - {` — `} - {summary.transcriptModel || ""} -
-
- )} - {hasLabel && ( -
- {summary.label || ( - - — - - )} -
- )} - -
- {!summary.scanError && ( - - )} -
- {hasValidations && ( -
- -
- )} - {hasErrors && ( -
- {summary.scanError && ( - - )} -
- )} -
- ); - - const handleClick = (e: React.MouseEvent) => { - // Don't navigate if clicking an inner link - if ((e.target as HTMLElement).closest("a")) { - return; - } - if (!scanResultUrl) { - return; - } - void navigate(scanResultUrl); - }; - - return isNavigable ? ( -
- {grid} -
- ) : ( - grid - ); -}; - -// memoize the component to avoid unnecessary re-renders (esp of things which may involve markdown rendering) -export const ScannerResultsRow = memo(ScannerResultsRowComponent); diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsBody.module.css b/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsBody.module.css deleted file mode 100644 index 3b3c57ff4..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsBody.module.css +++ /dev/null @@ -1,17 +0,0 @@ -.scannerHeaderRow { - display: grid; - grid-template-columns: auto auto; - column-gap: 0.5em; - margin-bottom: 1em; -} - -.controls { - display: flex; - align-items: flex-end; - justify-content: flex-end; - margin-bottom: 0.5em; -} - -.scrollContainer { - overflow-y: auto; -} diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsBody.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsBody.tsx deleted file mode 100644 index f34084058..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsBody.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { AllCommunityModule, ModuleRegistry } from "ag-grid-community"; -import { ColumnTable } from "arquero"; -import clsx from "clsx"; -import { FC, useMemo } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { DataframeView } from "../../../../components/DataframeView"; -import { ErrorPanel } from "../../../../components/ErrorPanel"; -import { NoContentsPanel } from "../../../../components/NoContentsPanel"; -import { useLoggingNavigate } from "../../../../debugging/navigationDebugging"; -import { scanResultRoute } from "../../../../router/url"; -import { useStore } from "../../../../state/store"; -import { Status } from "../../../../types/api-types"; -import { useScanRoute } from "../../../hooks/useScanRoute"; -import { kSegmentDataframe, kSegmentList } from "../../ScanPanelBody"; -import { ScannerResultsList } from "../list/ScannerResultsList"; -import { defaultColumns } from "../types"; - -import styles from "./ScannerResultsBody.module.css"; - -const columnOrder = ["transcript_id", "value", "explanation", "metadata"]; - -// Register AG Grid modules -ModuleRegistry.registerModules([AllCommunityModule]); - -export const ScannerResultsBody: FC<{ - selectedScan: Status; - scannerId: string; - selectedScanner: { - columnTable: ColumnTable | undefined; - isLoading: boolean; - error: string | undefined; - }; -}> = ({ - scannerId, - selectedScan, - selectedScanner: { columnTable, error, isLoading: isLoadingData }, -}) => { - const selectedResultsView = - useStore((state) => state.selectedResultsView) || kSegmentList; - - const hasScanner = (columnTable?.numRows() || 0) > 0; - const dataframeWrapText = useStore((state) => state.dataframeWrapText); - const setVisibleScannerResultsCount = useStore( - (state) => state.setVisibleScannerResultsCount - ); - - // Navigation setup - const navigate = useLoggingNavigate("ScannerResultsBody"); - const [searchParams] = useSearchParams(); - const { scansDir, scanPath } = useScanRoute(); - - const dataframeFilterColumns = useStore( - (state) => state.dataframeFilterColumns - ); - - const sortedColumns = useMemo(() => { - const cols = dataframeFilterColumns || defaultColumns; - return [...cols].sort(sortColumns); - }, [dataframeFilterColumns]); - - return ( -
- {hasScanner && ( -
- {selectedResultsView === kSegmentList && ( - - )} - {selectedResultsView === kSegmentDataframe && ( - { - // Navigate to the result detail view - const identifier = (row as { identifier?: string }).identifier; - if (identifier && scansDir) { - const route = scanResultRoute( - scansDir, - scanPath, - identifier, - searchParams - ); - void navigate(route); - } - }} - onVisibleRowCountChanged={setVisibleScannerResultsCount} - /> - )} -
- )} - {!hasScanner && !isLoadingData && !error && ( - - )} - {error && ( - - )} -
- ); -}; - -const sortColumns = (a: string, b: string) => { - const indexA = columnOrder.indexOf(a); - const indexB = columnOrder.indexOf(b); - if (indexA === -1 && indexB === -1) { - // leave in natural order - return 0; - } else if (indexA === -1) { - return 1; - } else if (indexB === -1) { - return -1; - } else { - return indexA - indexB; - } -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsFilter.module.css b/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsFilter.module.css deleted file mode 100644 index ecefa231a..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsFilter.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.flex { - display: flex; -} - -.label { - align-self: center; - margin-right: 0.3em; - margin-left: 0.2em; -} diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsFilter.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsFilter.tsx deleted file mode 100644 index c4e2b2c41..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsFilter.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import clsx from "clsx"; -import { ChangeEvent, FC, useCallback } from "react"; - -import { useStore } from "../../../../state/store"; - -import styles from "./ScannerResultsFilter.module.css"; - -export const kFilterPositiveResults = "positive_results"; -export const kFilterAllResults = "all_results"; - -export const ScannerResultsFilter: FC = () => { - const setSelectedFilter = useStore((state) => state.setSelectedFilter); - const selectedFilter = useStore((state) => state.selectedFilter); - - const handleChange = useCallback( - (e: ChangeEvent) => { - const sel = e.target as HTMLSelectElement; - setSelectedFilter(sel.value); - }, - [setSelectedFilter] - ); - - const options = [ - { label: "Positive", val: kFilterPositiveResults }, - { label: "All", val: kFilterAllResults }, - ]; - - return ( -
- - Results: - - -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsGroup.module.css b/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsGroup.module.css deleted file mode 100644 index ecefa231a..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsGroup.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.flex { - display: flex; -} - -.label { - align-self: center; - margin-right: 0.3em; - margin-left: 0.2em; -} diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsGroup.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsGroup.tsx deleted file mode 100644 index 27425b0d6..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsGroup.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import clsx from "clsx"; -import { ChangeEvent, FC, useCallback } from "react"; - -import { useStore } from "../../../../state/store"; -import { ResultGroup } from "../../../types"; - -import styles from "./ScannerResultsGroup.module.css"; - -interface ScannerResultsGroupProps { - options: Array; -} - -export const ScannerResultsGroup: FC = ({ - options = ["source", "label", "id", "epoch", "model"], -}) => { - const setGroupResultsBy = useStore((state) => state.setGroupResultsBy); - const groupResultsBy = useStore((state) => state.groupResultsBy); - - const handleChange = useCallback( - (e: ChangeEvent) => { - const sel = e.target as HTMLSelectElement; - setGroupResultsBy(sel.value as ResultGroup); - }, - [setGroupResultsBy] - ); - - const groupByOpts = [ - { label: "Source", value: "source" }, - { label: "Label", value: "label" }, - { label: "Id", value: "id" }, - { label: "Epoch", value: "epoch" }, - { label: "Model", value: "model" }, - ].filter((opt) => options.includes(toVal(opt.value))); - if (groupByOpts.length === 0) { - return null; - } - groupByOpts.unshift({ label: "None", value: "none" }); - - return ( -
- - Group: - - -
- ); -}; - -const toVal = (v: string | null): ResultGroup => { - if (v === "source") { - return "source"; - } else if (v === "label") { - return "label"; - } else if (v === "id") { - return "id"; - } else if (v === "epoch") { - return "epoch"; - } else if (v === "model") { - return "model"; - } else { - return "none"; - } -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsSearch.module.css b/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsSearch.module.css deleted file mode 100644 index c82ae1775..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsSearch.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.searchBox { - font-size: var(--inspect-font-size-smallest); -} diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsSearch.tsx b/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsSearch.tsx deleted file mode 100644 index 6c647d710..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/results/ScannerResultsSearch.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ChangeEvent, FC, useCallback } from "react"; - -import { ApplicationIcons } from "../../../../components/icons"; -import { TextInput } from "../../../../components/TextInput"; -import { useStore } from "../../../../state/store"; - -import styles from "./ScannerResultsSearch.module.css"; - -export const ScannerResultsSearch: FC = () => { - const scansSearchText = useStore((state) => state.scansSearchText); - const setScansSearchText = useStore((state) => state.setScansSearchText); - - const handleChange = useCallback((e: ChangeEvent) => { - setScansSearchText(e.target.value); - // TODO: lint react-hooks/exhaustive-deps - refactor to avoid the lint - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scan/scanners/types.ts b/src/inspect_scout/_view/www/src/app/scan/scanners/types.ts deleted file mode 100644 index fa4014abf..000000000 --- a/src/inspect_scout/_view/www/src/app/scan/scanners/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const defaultColumns: string[] = [ - "transcript_id", - "value", - "explanation", - "metadata", - "transcript_source_id", - "transcript_metadata", - "input_ids", - "input", - "label", - "uuid", - "message_references", - "validation_target", - "validation_result", - "scan_total_tokens", - "scan_model_usage", - "scan_events", - "timestamp", -]; diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultHeader.module.css b/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultHeader.module.css deleted file mode 100644 index baf2fd860..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultHeader.module.css +++ /dev/null @@ -1,46 +0,0 @@ -.header { - padding-left: 1rem; - padding-right: 1rem; - display: grid; - padding-bottom: 1rem; - padding-top: 1rem; - border-top: solid var(--bs-light-border-subtle) 1px; - column-gap: 1rem; - grid-template-rows: max-content max-content; -} - -.value { - word-wrap: break-all; - overflow-wrap: break-word; -} - -.oneCol { - grid-template-columns: minmax(0, auto); -} - -.twoCol { - grid-template-columns: minmax(0, auto) minmax(0, auto); -} - -.threeCol { - grid-template-columns: minmax(0, auto) minmax(0, auto) minmax(0, auto); -} - -.fourCol { - grid-template-columns: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax( - 0, - auto - ); -} - -.fiveCol { - grid-template-columns: - minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) - minmax(0, auto); -} - -.sixCol { - grid-template-columns: - minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) - minmax(0, auto) minmax(0, auto); -} diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultHeader.tsx b/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultHeader.tsx deleted file mode 100644 index e6df2dba7..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultHeader.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import clsx from "clsx"; -import { FC, ReactNode } from "react"; - -import { EventType } from "../../components/transcript/types"; -import { - Event, - ChatMessage, - Status, - Transcript, - AppConfig, -} from "../../types/api-types"; -import { TaskName } from "../components/TaskName"; -import { projectOrAppAliasedPath } from "../server/useAppConfig"; -import { - ScanResultInputData, - isEventInput, - isEventsInput, - isMessageInput, - isMessagesInput, - isTranscriptInput, - MessageType, -} from "../types"; - -import styles from "./ScannerResultHeader.module.css"; - -interface ScannerResultHeaderProps { - scan?: Status; - inputData?: ScanResultInputData; - appConfig: AppConfig; -} - -interface Column { - label: string; - value: ReactNode; - className?: string | string[]; -} - -export const ScannerResultHeader: FC = ({ - scan, - inputData, - appConfig, -}) => { - const columns = colsForResult(appConfig, inputData, scan) || []; - - return ( -
- {columns.map((col) => { - return ( -
- {col.label} -
- ); - })} - - {columns.map((col) => { - return ( -
- {col.value} -
- ); - })} -
- ); -}; - -const classForCols = (numCols: number) => { - return clsx( - numCols === 1 - ? styles.oneCol - : numCols === 2 - ? styles.twoCol - : numCols === 3 - ? styles.threeCol - : numCols === 4 - ? styles.fourCol - : numCols === 5 - ? styles.fiveCol - : styles.sixCol - ); -}; - -const colsForResult: ( - appConfig: AppConfig, - inputData?: ScanResultInputData, - status?: Status -) => Column[] | undefined = (appConfig, inputData, status) => { - if (!inputData) { - return []; - } - if (isTranscriptInput(inputData)) { - return transcriptCols(appConfig, inputData.input, status); - } else if (isMessageInput(inputData)) { - return messageCols(inputData.input, status); - } else if (isMessagesInput(inputData)) { - return messagesCols(inputData.input); - } else if (isEventInput(inputData)) { - return eventCols(inputData.input); - } else if (isEventsInput(inputData)) { - return eventsCols(inputData.input); - } else { - return []; - } -}; - -const transcriptCols = ( - appConfig: AppConfig, - transcript: Transcript, - status?: Status -) => { - // Read values from the transcript directly, falling back to metadata - // The metadata was previously used to store these values before they were - // added to the main Transcript schema (so we're doing this mainly for backwards - // compatibility with old scan results) - // Source info - const sourceUri = - transcript.source_uri || - (transcript.metadata?.log as string | undefined) || - ""; - - // Coerce this to a URI - let resolvedSourceUrl = sourceUri; - if (resolvedSourceUrl && resolvedSourceUrl.startsWith("/")) { - resolvedSourceUrl = `file://${resolvedSourceUrl}`; - } - const displaySourceUri = projectOrAppAliasedPath( - appConfig, - resolvedSourceUrl - ); - - // Model info - const transcriptModel = - transcript.model || - (transcript.metadata?.model as string | undefined) || - ""; - const scanningModel = status?.spec.model?.model; - - // Task information - const taskSet = - transcript.task_set || - (transcript.metadata?.task_name as string | undefined) || - ""; - const taskId = - transcript.task_id || (transcript.metadata?.id as string | undefined) || ""; - const taskRepeat = - transcript.task_repeat || (transcript.metadata?.epoch as number) || -1; - - const cols: Column[] = [ - { - label: "Task", - value: ( - - ), - }, - { - label: "Source", - value: displaySourceUri, - }, - { - label: "Model", - value: transcriptModel, - }, - ]; - - if (status?.spec.model?.model) { - cols.push({ - label: "Scanning Model", - value: scanningModel, - }); - } - - return cols; -}; - -const messageCols = (message: MessageType, status?: Status) => { - const cols: Column[] = [ - { - label: "Message ID", - value: message.id, - }, - ]; - - if (message.role === "assistant") { - cols.push({ - label: "Model", - value: message.model, - }); - cols.push({ - label: "Tool Calls", - value: ((message.tool_calls as []) || []).length, - }); - } else { - cols.push({ - label: "Role", - value: message.role, - }); - } - - if (status?.spec.model?.model) { - cols.push({ - label: "Scanning Model", - value: status.spec.model.model, - }); - } - - return cols; -}; - -const messagesCols = (messages: ChatMessage[]): Column[] => { - return [ - { - label: "Message Count", - value: messages.length, - }, - ]; -}; - -const eventCols = (event: EventType): Column[] => { - return [ - { - label: "Event Type", - value: event.event, - }, - { - label: "Timestamp", - value: event.timestamp - ? new Date(event.timestamp).toLocaleString() - : undefined, - }, - ]; -}; - -const eventsCols = (events: Event[]): Column[] => { - return [ - { - label: "Event Count", - value: events.length, - }, - ]; -}; diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultNav.module.css b/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultNav.module.css deleted file mode 100644 index a8f5f16c0..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultNav.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.resultNav { - display: grid; - grid-template-columns: max-content auto max-content; - align-items: center; - gap: 0.5rem; - margin-right: 1em; -} - -.nav:hover:not(.disabled) { - cursor: pointer; -} - -.disabled { - opacity: 0.5; - pointer-events: none; -} diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultNav.tsx b/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultNav.tsx deleted file mode 100644 index becbab26e..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultNav.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { FC, useMemo } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { useLoggingNavigate } from "../../debugging/navigationDebugging"; -import { scanResultRoute } from "../../router/url"; -import { useStore } from "../../state/store"; -import { NextPreviousNav } from "../components/NextPreviousNav"; -import { useScanRoute } from "../hooks/useScanRoute"; -import { IdentifierInfo, resultIdentifier } from "../utils/results"; - -export const ScannerResultNav: FC = () => { - const navigate = useLoggingNavigate("ScannerResultNav"); - const [searchParams] = useSearchParams(); - const { scansDir, scanPath, scanResultUuid } = useScanRoute(); - - const visibleScannerResults = useStore( - (state) => state.visibleScannerResults - ); - - const currentIndex = useMemo(() => { - if (!visibleScannerResults) { - return -1; - } - return visibleScannerResults.findIndex( - (s) => s.identifier === scanResultUuid - ); - }, [visibleScannerResults, scanResultUuid]); - - const hasPrevious = currentIndex > 0; - const hasNext = - visibleScannerResults && - currentIndex >= 0 && - currentIndex < visibleScannerResults.length - 1; - - const handlePrevious = () => { - if (!hasPrevious || !visibleScannerResults) { - return; - } - const previousResult = visibleScannerResults[currentIndex - 1]; - if (!scansDir) { - return; - } - const route = scanResultRoute( - scansDir, - scanPath, - previousResult?.identifier, - searchParams - ); - void navigate(route); - }; - - const handleNext = () => { - if (!hasNext || !visibleScannerResults) { - return; - } - const nextResult = visibleScannerResults[currentIndex + 1]; - if (!scansDir) { - return; - } - const route = scanResultRoute( - scansDir, - scanPath, - nextResult?.identifier, - searchParams - ); - void navigate(route); - }; - - const result = - visibleScannerResults && currentIndex !== -1 - ? visibleScannerResults[currentIndex] - : undefined; - - return ( - - - {visibleScannerResults && currentIndex !== -1 - ? printIdentifier(resultIdentifier(result), result?.label) - : undefined} - - - ); -}; - -const printIdentifier = ( - identifier: IdentifierInfo, - label?: string -): string => { - let val = ""; - if (identifier.epoch) { - val = `${identifier.id} epoch ${identifier.epoch}`; - } else { - val = String(identifier.id); - } - - if (label && label.length > 0) { - val += ` (${label})`; - } - return val; -}; diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultPanel.module.css b/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultPanel.module.css deleted file mode 100644 index 15cab2af1..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultPanel.module.css +++ /dev/null @@ -1,76 +0,0 @@ -.root { - height: 100%; - display: grid; - grid-template-rows: max-content max-content max-content 1fr; -} - -.scroller { - height: 100%; - width: 100%; - overflow-y: auto; -} - -.tabSet { - border-top: solid 1px var(--bs-border-color); - flex: 1; - min-height: 0; - overflow-y: hidden; - display: flex; - flex-direction: column; -} - -.tabControl { - margin: 0.2rem; -} - -.tabs { - margin-right: 1rem; -} - -.fullHeight { - height: 100%; -} - -.contentArea { - display: flex; - flex-direction: row; - flex: 1; - min-height: 0; - overflow: hidden; -} - -.tabSetWrapper { - flex: 1; - min-width: 0; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.withValidation { - border-top: solid 1px var(--bs-border-color); -} - -.splitLayout { - height: 100%; - width: 100%; -} - -.splitStart { - height: 100%; - min-width: 0; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.validationSidebar { - display: flex; - flex-direction: column; - height: 100%; - border-left: 1px solid var(--bs-border-color); - background-color: var(--bs-body-bg); - overflow-y: auto; -} diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultPanel.tsx b/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultPanel.tsx deleted file mode 100644 index 425795356..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/ScannerResultPanel.tsx +++ /dev/null @@ -1,393 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; -import { VscodeSplitLayout } from "@vscode-elements/react-elements"; -import { clsx } from "clsx"; -import { FC, ReactNode, useCallback, useEffect, useMemo } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { ExtendedFindProvider } from "../../components/ExtendedFindProvider"; -import { ApplicationIcons } from "../../components/icons"; -import JSONPanel from "../../components/JsonPanel"; -import { LoadingBar } from "../../components/LoadingBar"; -import { TabPanel, TabSet } from "../../components/TabSet"; -import { ToolButton } from "../../components/ToolButton"; -import { EventNode, EventType } from "../../components/transcript/types"; -import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import { - getScannerParam, - getValidationParam, - updateValidationParam, -} from "../../router/url"; -import { useStore } from "../../state/store"; -import { ScansNavbar } from "../components/ScansNavbar"; -import { useScanRoute } from "../hooks/useScanRoute"; -import { useSelectedScan } from "../hooks/useSelectedScan"; -import { useSelectedScanResultData } from "../hooks/useSelectedScanResultData"; -import { useSelectedScanResultInputData } from "../hooks/useSelectedScanResultInputData"; -import { useAppConfig } from "../server/useAppConfig"; -import { useHasTranscript } from "../server/useHasTranscript"; -import { isTranscriptInput, ScanResultData } from "../types"; -import { getScanDisplayName } from "../utils/scan"; -import { getTranscriptDisplayName } from "../utils/transcript"; -import { useScansDir } from "../utils/useScansDir"; -import { useTranscriptsDir } from "../utils/useTranscriptsDir"; -import { ValidationCaseEditor } from "../validation/components/ValidationCaseEditor"; - -import { ErrorPanel } from "./error/ErrorPanel"; -import { InfoPanel } from "./info/InfoPanel"; -import { MetadataPanel } from "./metadata/MetadataPanel"; -import { ResultPanel } from "./result/ResultPanel"; -import { ScannerResultHeader } from "./ScannerResultHeader"; -import { ScannerResultNav } from "./ScannerResultNav"; -import styles from "./ScannerResultPanel.module.css"; -import { TranscriptPanel } from "./transcript/TranscriptPanel"; - -const kTabIdResult = "Result"; -const kTabIdError = "Error"; -const kTabIdInput = "Input"; -const kTabIdInfo = "Info"; -const kTabIdJson = "JSON"; -const kTabIdTranscript = "transcript"; -const kTabIdMetadata = "Metadata"; - -export const ScannerResultPanel: FC = () => { - // Url data - const { scanResultUuid } = useScanRoute(); - const [searchParams, setSearchParams] = useSearchParams(); - - // Required server data - const { loading: scanLoading, data: selectedScan } = useSelectedScan(); - const { displayScansDir, resolvedScansDirSource, setScansDir } = - useScansDir(true); - // Sync URL query param with store state - const setSelectedScanner = useStore((state) => state.setSelectedScanner); - useEffect(() => { - const scannerParam = getScannerParam(searchParams); - if (scannerParam) { - setSelectedScanner(scannerParam); - } - }, [searchParams, setSelectedScanner]); - - // Sync displayed result with URL - this ensures both selectedScanResult - // (for list highlighting) and displayedScanResult (for route restoration) - // stay in sync with what's actually being viewed - const setSelectedScanResult = useStore( - (state) => state.setSelectedScanResult - ); - const setDisplayedScanResult = useStore( - (state) => state.setDisplayedScanResult - ); - useEffect(() => { - if (scanResultUuid) { - setSelectedScanResult(scanResultUuid); - setDisplayedScanResult(scanResultUuid); - } - }, [scanResultUuid, setSelectedScanResult, setDisplayedScanResult]); - - const appConfig = useAppConfig(); - - // Validation sidebar - URL is the source of truth - const validationSidebarCollapsed = !getValidationParam(searchParams); - - const toggleValidationSidebar = useCallback(() => { - setSearchParams((prevParams) => { - const isCurrentlyOpen = getValidationParam(prevParams); - return updateValidationParam(prevParams, !isCurrentlyOpen); - }); - }, [setSearchParams]); - - const selectedTab = useStore((state) => state.selectedResultTab); - const visibleScannerResults = useStore( - (state) => state.visibleScannerResults - ); - - const setSelectedResultTab = useStore((state) => state.setSelectedResultTab); - const { data: selectedResult, loading: resultLoading } = - useSelectedScanResultData(scanResultUuid); - - const { loading: inputLoading, data: inputData } = - useSelectedScanResultInputData(selectedResult?.uuid); - - // Set document title with task name and scan location - const taskName = - inputData && isTranscriptInput(inputData) - ? getTranscriptDisplayName(inputData.input) - : undefined; - useDocumentTitle( - taskName, - getScanDisplayName(selectedScan, appConfig.scans.dir), - "Scans" - ); - - const { resolvedTranscriptsDir } = useTranscriptsDir(false); - const { loading: hasTranscriptLoading, data: hasTranscript } = - useHasTranscript( - !selectedResult - ? skipToken - : { id: selectedResult.transcriptId, location: resolvedTranscriptsDir } - ); - - // Sync URL tab parameter with store on mount and URL changes - useEffect(() => { - const tabParam = searchParams.get("tab"); - if (tabParam) { - // Valid tab IDs - const validTabs = [ - kTabIdResult, - kTabIdInput, - kTabIdInfo, - kTabIdJson, - kTabIdTranscript, - ]; - if (validTabs.includes(tabParam)) { - setSelectedResultTab(tabParam); - } - } - }, [searchParams, setSelectedResultTab]); - - const handleTabChange = (tabId: string) => { - setSelectedResultTab(tabId); - const newParams = new URLSearchParams(searchParams); - newParams.set("tab", tabId); - setSearchParams(newParams); - }; - - // TODO: lint react-hooks/preserve-manual-memoization - the lint seems to be a bug in the rule that doesn't account for the ? - // However, this useMemo feels like a premature optimization. I think it should go - // eslint-disable-next-line react-hooks/preserve-manual-memoization - const showEvents = useMemo(() => { - if (!selectedResult?.scanEvents) { - return false; - } - - const hasNonSpanEvents = selectedResult.scanEvents.some((event) => { - return event.event !== "span_begin" && event.event !== "span_end"; - }); - - return hasNonSpanEvents; - }, [selectedResult?.scanEvents]); - - const hasError = - selectedResult?.scanError !== undefined && - selectedResult?.scanError !== null; - - const highlightLabeled = useStore((state) => state.highlightLabeled); - const setHighlightLabeled = useStore((state) => state.setHighlightLabeled); - const toggleHighlightLabeled = useCallback(() => { - setHighlightLabeled(!highlightLabeled); - }, [highlightLabeled, setHighlightLabeled]); - - const tools = useMemo(() => { - const toolButtons: ReactNode[] = []; - - // Existing highlight refs button (keep as-is) - if ( - selectedTab === kTabIdInput && - selectedResult?.inputType === "transcript" && - selectedResult?.messageReferences.length > 0 - ) { - toolButtons.push( - - ); - } - - // Validation button - only show when transcriptId is available - if (selectedResult?.transcriptId) { - toolButtons.push( - - ); - } - - return toolButtons; - }, [ - highlightLabeled, - toggleHighlightLabeled, - selectedTab, - selectedResult, - toggleValidationSidebar, - validationSidebarCollapsed, - ]); - - const renderTabSet = (resultData: ScanResultData) => ( - - {hasError ? ( - { - handleTabChange(kTabIdError); - }} - > - - - ) : undefined} - {!hasError ? ( - { - handleTabChange(kTabIdResult); - }} - className={styles.fullHeight} - > - {resultData && inputData && ( - - )} - - ) : undefined} - {showEvents ? ( - { - handleTabChange(kTabIdTranscript); - }} - > - - - ) : undefined} - { - handleTabChange(kTabIdMetadata); - }} - > - - - { - handleTabChange(kTabIdInfo); - }} - > - - - { - handleTabChange(kTabIdJson); - }} - > - - - - ); - - return ( -
- - {visibleScannerResults.length > 0 && } - - - - - {selectedResult && ( - -
- {validationSidebarCollapsed || !selectedResult.transcriptId ? ( -
- {renderTabSet(selectedResult)} -
- ) : ( - -
- {renderTabSet(selectedResult)} -
-
- -
-
- )} -
-
- )} -
- ); -}; - -const skipScanSpan = ( - nodes: EventNode[] -): EventNode[] => { - if (nodes.length === 1 && nodes[0]?.event.event === "span_begin") { - return nodes[0].children; - } - return nodes; -}; diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/error/ErrorPanel.module.css b/src/inspect_scout/_view/www/src/app/scannerResult/error/ErrorPanel.module.css deleted file mode 100644 index 1ab11126d..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/error/ErrorPanel.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.container { - margin: 0.5rem; -} - -.traceback { - margin-top: 1rem; -} diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/error/ErrorPanel.tsx b/src/inspect_scout/_view/www/src/app/scannerResult/error/ErrorPanel.tsx deleted file mode 100644 index 126569f44..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/error/ErrorPanel.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { clsx } from "clsx"; -import { FC } from "react"; - -import { ANSIDisplay } from "../../../components/AnsiDisplay"; -import { Card, CardBody, CardHeader } from "../../../components/Card"; - -import styles from "./ErrorPanel.module.css"; - -interface ErrorPanelProps { - error?: string; - traceback?: string; -} - -export const ErrorPanel: FC = ({ error, traceback }) => { - return ( - - Error - -
{error}
- - {traceback && ( - - )} -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/info/InfoPanel.module.css b/src/inspect_scout/_view/www/src/app/scannerResult/info/InfoPanel.module.css deleted file mode 100644 index e324c7172..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/info/InfoPanel.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.scanInfo { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - margin-bottom: 1rem; -} - -.container { - padding: 0.5rem; -} diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/info/InfoPanel.tsx b/src/inspect_scout/_view/www/src/app/scannerResult/info/InfoPanel.tsx deleted file mode 100644 index fcdef50b2..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/info/InfoPanel.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { Card, CardBody, CardHeader } from "../../../components/Card"; -import { MetaDataGrid } from "../../../components/content/MetaDataGrid"; -import { RecordTree } from "../../../components/content/RecordTree"; -import { LabeledValue } from "../../../components/LabeledValue"; -import { ModelTokenTable } from "../../../components/usage/ModelTokenTable"; -import { formatNumber } from "../../../utils/format"; -import { ScanResultData } from "../../types"; - -import styles from "./InfoPanel.module.css"; - -interface InfoPanelProps { - resultData?: ScanResultData; -} - -export const InfoPanel: FC = ({ resultData }) => { - return ( - resultData && ( -
- - - - - - - - - - - - - - - {resultData?.scanModelUsage && - Object.keys(resultData?.scanModelUsage).length > 0 && ( - - - - {Object.keys(resultData?.scanModelUsage).map((key) => { - if (!resultData?.scanModelUsage[key]) { - return null; - } - return ( - - ); - })} - - - )} - {resultData?.scanMetadata && - Object.keys(resultData.scanMetadata).length > 0 && ( - - - - - - - )} -
- ) - ); -}; - -export const ScannerInfoPanel: FC = ({ resultData }) => { - return ( -
-
- {resultData?.scannerName} - {resultData?.scannerFile && resultData.scannerFile !== null && ( - {resultData?.scannerFile} - )} - {(resultData?.scanTotalTokens || 0) > 0 && ( - - {resultData?.scanTotalTokens - ? formatNumber(resultData.scanTotalTokens) - : ""} - - )} -
- {resultData?.scanTags && resultData.scanTags.length > 0 && ( - - {(resultData?.scanTags || []).join(", ")} - - )} - {resultData?.scannerParams && - Object.keys(resultData.scannerParams).length > 0 && ( - - - - )} -
- ); -}; - -export const TranscriptInfoPanel: FC = ({ resultData }) => { - return ( -
- -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/metadata/Metadata.module.css b/src/inspect_scout/_view/www/src/app/scannerResult/metadata/Metadata.module.css deleted file mode 100644 index c71ee3cb1..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/metadata/Metadata.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.container { - padding: 1rem; - width: 100%; - height: 100%; -} diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/metadata/MetadataPanel.tsx b/src/inspect_scout/_view/www/src/app/scannerResult/metadata/MetadataPanel.tsx deleted file mode 100644 index ebfe01022..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/metadata/MetadataPanel.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { Card, CardBody } from "../../../components/Card"; -import { RecordTree } from "../../../components/content/RecordTree"; -import { LabeledValue } from "../../../components/LabeledValue"; -import { NoContentsPanel } from "../../../components/NoContentsPanel"; -import { ScanResultData } from "../../types"; - -import styles from "./Metadata.module.css"; - -interface MetadataPanelProps { - resultData?: ScanResultData; -} - -export const MetadataPanel: FC = ({ resultData }) => { - const hasMetadata = - resultData && Object.keys(resultData?.metadata).length > 0; - return ( - resultData && ( -
- {!hasMetadata && } - {hasMetadata && ( - - - - - - - - )} -
- ) - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultBody.module.css b/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultBody.module.css deleted file mode 100644 index 9e745ba6c..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultBody.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.container { - height: 100%; - overflow-y: hidden; - display: flex; - flex-direction: column; -} - -.scrollable { - padding: 0.5rem; - overflow-y: auto; - flex: 1; - min-height: 0; -} diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultBody.tsx b/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultBody.tsx deleted file mode 100644 index 8b17fa72e..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultBody.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import clsx from "clsx"; -import { FC, useCallback, useRef } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; - -import { ChatViewVirtualList } from "../../../components/chat/ChatViewVirtualList"; -import { ApplicationIcons } from "../../../components/icons"; -import { NoContentsPanel } from "../../../components/NoContentsPanel"; -import { TranscriptView } from "../../../components/transcript/TranscriptView"; -import { transcriptRoute } from "../../../router/url"; -import { useStore } from "../../../state/store"; -import { - ColumnHeader, - ColumnHeaderButton, -} from "../../components/ColumnHeader"; -import { - ScanResultInputData, - isEventInput, - isEventsInput, - isMessageInput, - isMessagesInput, - isTranscriptInput, - ScanResultData, -} from "../../types"; - -import styles from "./ResultBody.module.css"; - -export interface ResultBodyProps { - resultData: ScanResultData; - inputData: ScanResultInputData; - transcriptDir: string; - hasTranscript: boolean; -} - -export const ResultBody: FC = ({ - resultData, - inputData, - transcriptDir, - hasTranscript, -}) => { - const scrollRef = useRef(null); - const [searchParams] = useSearchParams(); - const navigate = useNavigate(); - - // Get message or event ID from query params - const initialMessageId = searchParams.get("message"); - const initialEventId = searchParams.get("event"); - - const highlightLabeled = useStore((state) => state.highlightLabeled); - - const handleNavigateToTranscript = useCallback(() => { - if (transcriptDir && resultData.transcriptId) { - void navigate(transcriptRoute(transcriptDir, resultData.transcriptId)); - } - }, [navigate, transcriptDir, resultData.transcriptId]); - - // Only show the transcript button when we have both transcriptsDir and transcriptId - const canNavigateToTranscript = - hasTranscript && transcriptDir.length > 0 && resultData.transcriptId; - const transcriptAction = canNavigateToTranscript ? ( - - ) : undefined; - - return ( -
- -
- -
-
- ); -}; - -interface InputRendererProps { - className?: string | string[]; - resultData?: ScanResultData; - inputData: ScanResultInputData; - scrollRef?: React.RefObject; - initialMessageId?: string | null; - initialEventId?: string | null; - highlightLabeled?: boolean; -} - -const InputRenderer: FC = ({ - resultData, - inputData, - className, - scrollRef, - initialMessageId, - initialEventId, - highlightLabeled, -}) => { - if (isTranscriptInput(inputData)) { - if (inputData.input.messages && inputData.input.messages.length > 0) { - const labels = resultData?.messageReferences.reduce( - (acc, ref) => { - if (ref.cite) { - acc[ref.id] = ref.cite; - } - return acc; - }, - {} as Record - ); - - return ( - - ); - } else if (inputData.input.events && inputData.input.events.length > 0) { - return ( - - ); - } else { - return ; - } - } else if (isMessagesInput(inputData)) { - return ( - - ); - } else if (isMessageInput(inputData)) { - return ( - - ); - } else if (isEventsInput(inputData)) { - return ( - - ); - } else if (isEventInput(inputData)) { - return ( - - ); - } else { - return
Unsupported Input Type
; - } -}; diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultPanel.module.css b/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultPanel.module.css deleted file mode 100644 index 34ff71e55..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultPanel.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.container { - display: grid; - grid-template-columns: 1fr 2fr; - overflow-y: hidden; - height: 100%; - width: 100%; - min-height: 0; -} - -.container > * { - min-height: 0; -} diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultPanel.tsx b/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultPanel.tsx deleted file mode 100644 index f8005f142..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultPanel.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { ScanResultData, ScanResultInputData } from "../../types"; - -import { ResultBody } from "./ResultBody"; -import styles from "./ResultPanel.module.css"; -import { ResultSidebar } from "./ResultSidebar"; - -interface ResultPanelProps { - resultData: ScanResultData; - inputData: ScanResultInputData | undefined; - transcriptDir: string; - hasTranscript: boolean; -} - -export const ResultPanel: FC = ({ - resultData, - inputData, - transcriptDir, - hasTranscript, -}) => ( -
- - {inputData ? ( - - ) : ( -
No Input Available
- )} -
-); diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultSidebar.module.css b/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultSidebar.module.css deleted file mode 100644 index 496dc44a8..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultSidebar.module.css +++ /dev/null @@ -1,42 +0,0 @@ -.sidebar { - border-right: solid 1px var(--bs-border-color); - padding: 1rem; - overflow-y: auto; -} - -.container { - display: grid; - grid-template-columns: max-content minmax(0, 1fr); - align-self: start; - gap: 0.5rem; -} - -.colspan { - grid-column: span 2; -} - -.explanation { - display: grid; - grid-template-columns: max-content 1fr; - column-gap: 1rem; - row-gap: 0.75rem; -} - -.scanValue { - padding-right: 0.5rem; -} - -.values { - display: flex; -} - -.validation { - margin-left: 1rem; - display: grid; - grid-template-columns: max-content max-content; -} - -.validationLabel { - padding-right: 0.5rem; - margin-top: 2px; -} diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultSidebar.tsx b/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultSidebar.tsx deleted file mode 100644 index 82c1b1c11..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/result/ResultSidebar.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { MetaDataGrid } from "../../../components/content/MetaDataGrid"; -import { MarkdownReference } from "../../../components/MarkdownDivWithReferences"; -import { NoContentsPanel } from "../../../components/NoContentsPanel"; -import { Explanation } from "../../components/Explanation"; -import { ValidationResult } from "../../components/ValidationResult"; -import { Value } from "../../components/Value"; -import { ScanResultData, ScanResultInputData } from "../../types"; -import { useMarkdownRefs } from "../../utils/refs"; - -import styles from "./ResultSidebar.module.css"; - -interface ResultSidebarProps { - inputData?: ScanResultInputData; - resultData?: ScanResultData; -} - -export const ResultSidebar: FC = ({ - inputData, - resultData, -}) => { - const refs: MarkdownReference[] = useMarkdownRefs(resultData, inputData); - - if (!resultData) { - return ; - } - - return ( -
-
- {resultData.label && ( - <> -
- Label -
-
{resultData.label}
- - )} -
- Value -
-
- - {resultData.validationResult !== undefined && - resultData.validationResult !== null ? ( -
-
- Validation -
- -
- ) : undefined} -
-
-
- Explanation -
- -
- {resultData.metadata && Object.keys(resultData.metadata).length > 0 && ( - <> -
- Metadata -
-
- -
- - )} -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/transcript/TranscriptPanel.module.css b/src/inspect_scout/_view/www/src/app/scannerResult/transcript/TranscriptPanel.module.css deleted file mode 100644 index b8b1ac0bc..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/transcript/TranscriptPanel.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.container { - overflow-y: auto; - width: 100%; - height: 100%; -} diff --git a/src/inspect_scout/_view/www/src/app/scannerResult/transcript/TranscriptPanel.tsx b/src/inspect_scout/_view/www/src/app/scannerResult/transcript/TranscriptPanel.tsx deleted file mode 100644 index 4e7ab68ec..000000000 --- a/src/inspect_scout/_view/www/src/app/scannerResult/transcript/TranscriptPanel.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import clsx from "clsx"; -import { FC, useRef } from "react"; - -import { TranscriptView } from "../../../components/transcript/TranscriptView"; -import { EventNode, EventType } from "../../../components/transcript/types"; -import { ScanResultData } from "../../types"; - -import styles from "./TranscriptPanel.module.css"; - -interface TranscriptPanelProps { - id: string; - resultData?: ScanResultData; - nodeFilter?: (node: EventNode[]) => EventNode[]; -} - -export const TranscriptPanel: FC = ({ - id, - resultData, - nodeFilter, -}) => { - const scrollRef = useRef(null); - - return ( -
- -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scans/ScansFilterBar.tsx b/src/inspect_scout/_view/www/src/app/scans/ScansFilterBar.tsx deleted file mode 100644 index a7d7aaac4..000000000 --- a/src/inspect_scout/_view/www/src/app/scans/ScansFilterBar.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { FC, useCallback } from "react"; - -import { ScalarValue } from "../../api/api"; -import { ScansTableState, useStore } from "../../state/store"; -import { useAddFilterPopover } from "../components/columnFilter"; -import { FilterBar, type ColumnInfo } from "../components/FilterBar"; -import { useFilterBarHandlers } from "../components/useFilterBarHandlers"; - -import { - COLUMN_LABELS, - COLUMN_HEADER_TITLES, - DEFAULT_COLUMN_ORDER, - DEFAULT_VISIBLE_COLUMNS, - FILTER_COLUMNS, - ScanColumnKey, -} from "./columns"; - -// Convert column definitions to ColumnInfo array -const COLUMNS_INFO: ColumnInfo[] = DEFAULT_COLUMN_ORDER.map((key) => ({ - id: key, - label: COLUMN_LABELS[key], - headerTitle: COLUMN_HEADER_TITLES[key], -})); - -export const ScansFilterBar: FC<{ - includeColumnPicker?: boolean; - filterSuggestions?: ScalarValue[]; - onFilterColumnChange?: (columnId: string | null) => void; -}> = ({ - includeColumnPicker = true, - filterSuggestions = [], - onFilterColumnChange, -}) => { - // Scans Filter State - const filters = useStore((state) => state.scansTableState.columnFilters); - const visibleColumns = useStore( - (state) => state.scansTableState.visibleColumns - ); - const setScansTableState = useStore((state) => state.setScansTableState); - - // Use shared filter bar handlers - const { handleFilterChange, removeFilter, handleAddFilter } = - useFilterBarHandlers({ - setTableState: setScansTableState, - defaultVisibleColumns: DEFAULT_VISIBLE_COLUMNS, - }); - - // Add filter popover state - const addFilterPopover = useAddFilterPopover({ - columns: FILTER_COLUMNS, - filters, - onAddFilter: handleAddFilter, - onFilterColumnChange, - }); - - // Handle visible columns change - const handleVisibleColumnsChange = useCallback( - (newVisibleColumns: string[]) => { - setScansTableState((prevState) => ({ - ...prevState, - visibleColumns: newVisibleColumns as ScanColumnKey[], - })); - }, - [setScansTableState] - ); - - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scans/ScansGrid.tsx b/src/inspect_scout/_view/www/src/app/scans/ScansGrid.tsx deleted file mode 100644 index 70f94e72c..000000000 --- a/src/inspect_scout/_view/www/src/app/scans/ScansGrid.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { FC, useEffect, useMemo, useRef } from "react"; - -import { ScalarValue } from "../../api/api"; -import { scanRoute } from "../../router/url"; -import { useStore } from "../../state/store"; -import type { ScanRow as ApiScanRow } from "../../types/api-types"; -import { toRelativePath } from "../../utils/path"; -import { DataGrid } from "../components/dataGrid"; - -import { - DEFAULT_COLUMN_ORDER, - getScanColumns, - ScanColumn, - ScanRow, -} from "./columns"; -import { useColumnSizing } from "./columnSizing"; - -// Generate a stable key for a scan item -function scanItemKey(index: number, item?: ScanRow): string { - if (!item) { - return String(index); - } - return item.scan_id; -} - -interface ScansGridProps { - scans: ApiScanRow[]; - resultsDir: string | undefined; - className?: string | string[]; - loading?: boolean; - /** Called when scroll position nears end */ - onScrollNearEnd?: (distanceFromBottom?: number) => void; - /** Whether more data is available to fetch */ - hasMore?: boolean; - /** Distance from bottom (in px) at which to trigger callback */ - fetchThreshold?: number; - /** Autocomplete suggestions for the currently editing filter column */ - filterSuggestions?: ScalarValue[]; - /** Called when a filter column starts/stops being edited */ - onFilterColumnChange?: (columnId: string | null) => void; -} - -export const ScansGrid: FC = ({ - scans, - resultsDir, - className, - loading, - onScrollNearEnd, - hasMore = false, - fetchThreshold = 500, - filterSuggestions = [], - onFilterColumnChange, -}) => { - // Table ref for DOM measurement (used by column sizing) - const tableRef = useRef(null); - - // Table state from store - const sorting = useStore((state) => state.scansTableState.sorting); - const columnOrder = useStore((state) => state.scansTableState.columnOrder); - const columnFilters = useStore( - (state) => state.scansTableState.columnFilters - ); - const rowSelection = useStore((state) => state.scansTableState.rowSelection); - const focusedRowId = useStore((state) => state.scansTableState.focusedRowId); - const visibleColumns = useStore( - (state) => state.scansTableState.visibleColumns - ); - const setTableState = useStore((state) => state.setScansTableState); - const setVisibleScanJobCount = useStore( - (state) => state.setVisibleScanJobCount - ); - - // Add computed relativeLocation to each scan - const data = useMemo( - (): ScanRow[] => - scans.map((scan) => ({ - ...scan, - relativeLocation: toRelativePath(scan.location, resultsDir), - })), - [scans, resultsDir] - ); - - // Update visible count - useEffect(() => { - setVisibleScanJobCount(data.length); - }, [data.length, setVisibleScanJobCount]); - - // Define table columns based on visible columns from store - const columns = useMemo( - () => getScanColumns(visibleColumns), - [visibleColumns] - ); - - // Column sizing with min/max constraints and auto-sizing - const { - columnSizing, - handleColumnSizingChange, - applyAutoSizing, - resetColumnSize, - } = useColumnSizing({ - columns, - tableRef, - data, - }); - - // Track if we've done initial auto-sizing - const hasInitializedRef = useRef(false); - - // Track previous visible columns to detect changes - const previousVisibleColumnsRef = useRef(null); - - // Auto-size columns on initial load when data is available - useEffect(() => { - if (!hasInitializedRef.current && data.length > 0) { - hasInitializedRef.current = true; - applyAutoSizing(); - } - }, [data.length, applyAutoSizing]); - - // Auto-size when visible columns change - // (applyAutoSizing preserves manually resized columns) - useEffect(() => { - const previousVisibleColumns = previousVisibleColumnsRef.current; - previousVisibleColumnsRef.current = visibleColumns; - - if (previousVisibleColumns && previousVisibleColumns !== visibleColumns) { - applyAutoSizing(); - } - }, [visibleColumns, applyAutoSizing]); - - // Compute effective column order - const effectiveColumnOrder = useMemo(() => { - if (columnOrder && columnOrder.length > 0) { - return columnOrder; - } - return DEFAULT_COLUMN_ORDER; - }, [columnOrder]); - - // Get row ID - const getRowId = (row: ScanRow): string => row.scan_id; - - // Get route for navigation - const getRowRoute = (row: ScanRow): string => { - if (!resultsDir) return ""; - return scanRoute(resultsDir, row.relativeLocation); - }; - - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scans/ScansPanel.module.css b/src/inspect_scout/_view/www/src/app/scans/ScansPanel.module.css deleted file mode 100644 index 2902ab9d3..000000000 --- a/src/inspect_scout/_view/www/src/app/scans/ScansPanel.module.css +++ /dev/null @@ -1,19 +0,0 @@ -.container { - display: grid; - grid-template-rows: max-content max-content 1fr max-content; - height: 100%; - row-gap: 0; -} - -.gridContainer { - display: flex; - flex-direction: column; - min-height: 0; - overflow: hidden; -} - -.grid { - flex: 1; - min-height: 0; - overflow: auto; -} diff --git a/src/inspect_scout/_view/www/src/app/scans/ScansPanel.tsx b/src/inspect_scout/_view/www/src/app/scans/ScansPanel.tsx deleted file mode 100644 index 711dc0016..000000000 --- a/src/inspect_scout/_view/www/src/app/scans/ScansPanel.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { clsx } from "clsx"; -import { FC, useCallback, useEffect, useMemo } from "react"; - -import { ErrorPanel } from "../../components/ErrorPanel"; -import { ExtendedFindProvider } from "../../components/ExtendedFindProvider"; -import { ApplicationIcons } from "../../components/icons"; -import { LoadingBar } from "../../components/LoadingBar"; -import { NoContentsPanel } from "../../components/NoContentsPanel"; -import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import { useStore } from "../../state/store"; -import { ScanRow } from "../../types/api-types"; -import { Footer } from "../components/Footer"; -import { ScansNavbar } from "../components/ScansNavbar"; -import { useScanFilterConditions } from "../hooks/useScanFilterConditions"; -import { useScansFilterBarProps } from "../hooks/useScansFilterBarProps"; -import { useAppConfig } from "../server/useAppConfig"; -import { useScansInfinite } from "../server/useScansInfinite"; -import { useScansDir } from "../utils/useScansDir"; - -import { SCANS_INFINITE_SCROLL_CONFIG } from "./constants"; -import { ScansFilterBar } from "./ScansFilterBar"; -import { ScansGrid } from "./ScansGrid"; -import styles from "./ScansPanel.module.css"; - -export const ScansPanel: FC = () => { - useDocumentTitle("Scans"); - - const config = useAppConfig(); - const scanDir = config.scans.dir; - const { - displayScansDir, - resolvedScansDir, - resolvedScansDirSource, - setScansDir, - } = useScansDir(); - - // Get filter condition and sorting from store - const condition = useScanFilterConditions(); - const sorting = useStore((state) => state.scansTableState.sorting); - - // Get autocomplete props for filter bar - const { filterSuggestions, onFilterColumnChange } = - useScansFilterBarProps(resolvedScansDir); - - // Load scans data with filtering and sorting - const { data, error, fetchNextPage, hasNextPage, isFetching } = - useScansInfinite( - resolvedScansDir, - SCANS_INFINITE_SCROLL_CONFIG.pageSize, - condition, - sorting - ); - - // Flatten pages into scans array - const scans: ScanRow[] = useMemo( - () => data?.pages.flatMap((page) => page.items) ?? [], - [data] - ); - - // Handle infinite scroll - const handleScrollNearEnd = useCallback(() => { - fetchNextPage({ cancelRefetch: false }).catch(console.error); - }, [fetchNextPage]); - - // Clear scan state from store on mount - const clearScansState = useStore((state) => state.clearScansState); - useEffect(() => { - clearScansState(); - }, [clearScansState]); - - return ( -
- - - - {error && ( - - )} - {!data && !error && ( - - )} - {data && !error && ( -
- - -
- )} -
- -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/scans/columnSizing/index.ts b/src/inspect_scout/_view/www/src/app/scans/columnSizing/index.ts deleted file mode 100644 index f00272725..000000000 --- a/src/inspect_scout/_view/www/src/app/scans/columnSizing/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -export * from "./useColumnSizing"; - -// Re-export shared utilities and strategies for convenience -export { - clampSize, - getColumnConstraints, - getColumnId, - getSizingStrategy, - sizingStrategies, - DEFAULT_MAX_SIZE, - DEFAULT_MIN_SIZE, - DEFAULT_SIZE, -} from "../../components/columnSizing"; - -export type { - ColumnSizeConstraints, - ColumnSizingStrategyKey, - SizingStrategy, - SizingStrategyContext, -} from "../../components/columnSizing"; diff --git a/src/inspect_scout/_view/www/src/app/scans/columnSizing/useColumnSizing.ts b/src/inspect_scout/_view/www/src/app/scans/columnSizing/useColumnSizing.ts deleted file mode 100644 index 71b367d35..000000000 --- a/src/inspect_scout/_view/www/src/app/scans/columnSizing/useColumnSizing.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { ColumnSizingState, OnChangeFn } from "@tanstack/react-table"; -import { useCallback, useEffect, useMemo, useRef } from "react"; - -import { useStore } from "../../../state/store"; -import { - clampSize, - ColumnSizingStrategyKey, - getColumnConstraints, - getSizingStrategy, - SizingStrategy, -} from "../../components/columnSizing"; -import { ScanColumn, ScanRow } from "../columns"; - -interface UseColumnSizingOptions { - /** Column definitions */ - columns: ScanColumn[]; - /** Reference to the table element for DOM measurement */ - tableRef: React.RefObject; - /** Current data for content measurement */ - data: ScanRow[]; -} - -interface UseColumnSizingResult { - /** Current column sizing state */ - columnSizing: ColumnSizingState; - /** Handler for column sizing changes with min/max enforcement */ - handleColumnSizingChange: OnChangeFn; - /** Current sizing strategy key */ - sizingStrategy: ColumnSizingStrategyKey; - /** Set the sizing strategy */ - setSizingStrategy: (strategy: ColumnSizingStrategyKey) => void; - /** Apply auto-sizing based on current strategy (preserves manually resized columns) */ - applyAutoSizing: () => void; - /** Reset a single column to its auto-calculated size */ - resetColumnSize: (columnId: string) => void; - /** Reset all column sizing and clear manual resize tracking */ - clearColumnSizing: () => void; -} - -/** - * Hook for managing column sizing with min/max constraints and auto-sizing. - * Manually resized columns are preserved during auto-sizing operations. - */ -export function useColumnSizing({ - columns, - tableRef, - data, -}: UseColumnSizingOptions): UseColumnSizingResult { - const columnSizing = useStore((state) => state.scansTableState.columnSizing); - const sizingStrategy = useStore( - (state) => state.scansTableState.sizingStrategy - ); - const manuallyResizedColumns = useStore( - (state) => state.scansTableState.manuallyResizedColumns - ); - const setTableState = useStore((state) => state.setScansTableState); - - // Track which columns have been manually resized - const manuallyResizedSet = useMemo( - () => new Set(manuallyResizedColumns), - [manuallyResizedColumns] - ); - - // Get constraints for all columns - const columnConstraints = useMemo( - () => getColumnConstraints(columns), - [columns] - ); - - // Track if we're in the middle of an auto-sizing operation - const isAutoSizingRef = useRef(false); - - // Store latest values in refs for stable callbacks - const latestRef = useRef({ - sizingStrategy, - columns, - data, - columnConstraints, - manuallyResizedSet, - columnSizing, - }); - - // Update refs when values change - useEffect(() => { - latestRef.current = { - sizingStrategy, - columns, - data, - columnConstraints, - manuallyResizedSet, - columnSizing, - }; - }); - - // Handle column sizing changes with min/max enforcement - const handleColumnSizingChange: OnChangeFn = useCallback( - (updaterOrValue) => { - const newSizing = - typeof updaterOrValue === "function" - ? updaterOrValue(columnSizing) - : updaterOrValue; - - // Clamp sizes to min/max constraints - const clampedSizing: ColumnSizingState = {}; - const newManuallyResized = new Set(manuallyResizedSet); - - for (const [columnId, size] of Object.entries(newSizing)) { - const constraints = columnConstraints.get(columnId); - if (constraints) { - clampedSizing[columnId] = clampSize(size, constraints); - } else { - clampedSizing[columnId] = size; - } - - // Mark this column as manually resized (unless we're auto-sizing) - if (!isAutoSizingRef.current) { - newManuallyResized.add(columnId); - } - } - - // Include existing sizes that weren't updated - for (const [columnId, size] of Object.entries(columnSizing)) { - if (!(columnId in clampedSizing)) { - clampedSizing[columnId] = size; - } - } - - setTableState((prev) => ({ - ...prev, - columnSizing: clampedSizing, - manuallyResizedColumns: isAutoSizingRef.current - ? prev.manuallyResizedColumns - : Array.from(newManuallyResized), - })); - }, - [columnSizing, columnConstraints, manuallyResizedSet, setTableState] - ); - - // Set sizing strategy - const setSizingStrategy = useCallback( - (strategy: ColumnSizingStrategyKey) => { - setTableState((prev) => ({ - ...prev, - sizingStrategy: strategy, - })); - }, - [setTableState] - ); - - // Apply auto-sizing based on current strategy - // Preserves sizes of manually resized columns - const applyAutoSizing = useCallback(() => { - isAutoSizingRef.current = true; - - try { - const { - sizingStrategy: strategyKey, - columns: cols, - data: rowData, - columnConstraints: constraints, - manuallyResizedSet: resizedSet, - columnSizing: currentSizing, - } = latestRef.current; - - const strategy = getSizingStrategy( - strategyKey - ) as SizingStrategy; - const calculatedSizing = strategy.computeSizes({ - tableElement: tableRef.current, - columns: cols, - data: rowData, - constraints, - }); - - // Merge: use calculated sizes for non-manually-resized columns, - // preserve existing sizes for manually resized columns - const newSizing: ColumnSizingState = {}; - for (const [columnId, size] of Object.entries(calculatedSizing)) { - if (resizedSet.has(columnId) && currentSizing[columnId] !== undefined) { - // Preserve manually resized column's current size - newSizing[columnId] = currentSizing[columnId]; - } else { - // Use calculated size - newSizing[columnId] = size; - } - } - - setTableState((prev) => ({ - ...prev, - columnSizing: newSizing, - })); - } finally { - isAutoSizingRef.current = false; - } - }, [tableRef, setTableState]); - - // Reset a single column to its auto-calculated size - const resetColumnSize = useCallback( - (columnId: string) => { - isAutoSizingRef.current = true; - - try { - const { - sizingStrategy: strategyKey, - columns: cols, - data: rowData, - columnConstraints: constraints, - } = latestRef.current; - - const strategy = getSizingStrategy( - strategyKey - ) as SizingStrategy; - const allSizes = strategy.computeSizes({ - tableElement: tableRef.current, - columns: cols, - data: rowData, - constraints, - }); - - const newSize = allSizes[columnId]; - if (newSize !== undefined) { - setTableState((prev) => { - // Remove this column from manually resized list - const newManuallyResized = prev.manuallyResizedColumns.filter( - (id) => id !== columnId - ); - - return { - ...prev, - columnSizing: { - ...prev.columnSizing, - [columnId]: newSize, - }, - manuallyResizedColumns: newManuallyResized, - }; - }); - } - } finally { - isAutoSizingRef.current = false; - } - }, - [tableRef, setTableState] - ); - - // Reset all column sizing and clear manual resize tracking - const clearColumnSizing = useCallback(() => { - setTableState((prev) => ({ - ...prev, - columnSizing: {}, - manuallyResizedColumns: [], - })); - }, [setTableState]); - - return { - columnSizing, - setSizingStrategy, - clearColumnSizing, - sizingStrategy, - handleColumnSizingChange, - applyAutoSizing, - resetColumnSize, - }; -} diff --git a/src/inspect_scout/_view/www/src/app/scans/columns.module.css b/src/inspect_scout/_view/www/src/app/scans/columns.module.css deleted file mode 100644 index 02bcc3511..000000000 --- a/src/inspect_scout/_view/www/src/app/scans/columns.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.green { - color: var(--bs-success); -} - -.yellow { - color: var(--bs-warning); -} - -.red { - color: var(--bs-danger); -} - -.blue { - color: var(--bs-primary); -} diff --git a/src/inspect_scout/_view/www/src/app/scans/columns.tsx b/src/inspect_scout/_view/www/src/app/scans/columns.tsx deleted file mode 100644 index a0ce635d7..000000000 --- a/src/inspect_scout/_view/www/src/app/scans/columns.tsx +++ /dev/null @@ -1,578 +0,0 @@ -import clsx from "clsx"; - -import { ApplicationIcons } from "../../components/icons"; -import type { ScanRow as ApiScanRow } from "../../types/api-types"; -import { formatNumber } from "../../utils/format"; -import { printObject } from "../../utils/object"; -import type { AvailableColumn } from "../components/columnFilter"; -import { ExtendedColumnDef, BaseColumnMeta } from "../components/columnTypes"; - -import styles from "./columns.module.css"; - -/** - * Scan row type for grid display. - * Extends the API ScanRow with a client-computed relativeLocation. - */ -export type ScanRow = ApiScanRow & { - /** Relative path from results directory (computed client-side) */ - relativeLocation: string; -}; - -// Define the keys that correspond to our scan columns -// These match the database column names for filtering to work -export type ScanColumnKey = - // Original columns - | "status" // Status column - | "scan_name" // Name column - | "scanners" - | "scan_id" - | "model" - | "location" // Path column - | "timestamp" // Time column - // New columns from spec - | "scan_file" - | "tags" - | "revision_version" - | "revision_commit" - | "revision_origin" - | "packages" - | "metadata" - | "scan_args" - | "transcript_count" - // Aggregated summary columns - | "total_results" - | "total_errors" - | "total_tokens"; - -// Column type for scan grid -export type ScanColumn = ExtendedColumnDef; - -// Column headers for display (used in column picker and add filter dropdown) -export const COLUMN_LABELS: Record = { - // Original columns - status: "Status", - scan_name: "Name", - scanners: "Scanners", - scan_id: "Scan ID", - model: "Model", - location: "Path", - timestamp: "Time", - // New columns from spec - scan_file: "Scan File", - tags: "Tags", - revision_version: "Version", - revision_commit: "Commit", - revision_origin: "Origin", - packages: "Packages", - metadata: "Metadata", - scan_args: "Scan Args", - transcript_count: "Transcripts", - // Aggregated summary columns - total_results: "Total Results", - total_errors: "Scanner Errors", - total_tokens: "Total Tokens", -}; - -// Column header tooltips -export const COLUMN_HEADER_TITLES: Record = { - // Original columns - status: "Scan completion status (complete, in progress, or error)", - scan_name: "Name of the scan configuration", - scanners: "List of scanners used in this scan", - scan_id: "Unique identifier for the scan", - model: "Model used for scanning", - location: "Path to the scan results", - timestamp: "Timestamp when the scan was started", - // New columns from spec - scan_file: "Source file path for the scan job", - tags: "Tags associated with this scan", - revision_version: "Git version of the scan code", - revision_commit: "Git commit hash of the scan code", - revision_origin: "Git origin URL of the scan code", - packages: "Package versions used in this scan", - metadata: "Custom metadata for this scan", - scan_args: "Arguments passed to the scan", - // Aggregated summary columns - total_results: "Total number of results across all scanners", - total_errors: "Errors reported by scanners while processing transcripts", - total_tokens: "Total tokens used across all scanners", - transcript_count: "Number of transcripts processed in this scan", -}; - -// Helper to get status display (icon and color) for a scan -function getStatusDisplay(scan: ScanRow): { - icon: string; - colorClass: string | undefined; -} { - switch (scan.status) { - case "complete": - return { icon: ApplicationIcons.success, colorClass: styles.green }; - case "error": - return { icon: ApplicationIcons.error, colorClass: styles.red }; - case "active": - case "incomplete": - default: - return { icon: ApplicationIcons.pendingTask, colorClass: styles.yellow }; - } -} - -// Helper to format object values for display -function formatObjectValue( - value: Record | null | undefined, - maxLength: number = 1000 -): string { - if (!value || Object.keys(value).length === 0) { - return "-"; - } - try { - return printObject(value, maxLength); - } catch { - return "-"; - } -} - -// Helper to get full JSON for tooltip -function getObjectTitleValue( - value: Record | null | undefined -): string { - if (!value || Object.keys(value).length === 0) { - return ""; - } - return JSON.stringify(value, null, 2); -} - -// All available columns, keyed by their ID (using database column names) -export const ALL_COLUMNS: Record = { - // ============================================ - // Original columns - // ============================================ - status: { - id: "status", - accessorKey: "status", - header: "✓", - headerTitle: COLUMN_HEADER_TITLES.status, - size: 70, - minSize: 70, - maxSize: 70, - meta: { - align: "center", - filterable: true, - filterType: "string", - }, - cell: (info) => { - const scan = info.row.original; - - if (scan.active_completion_pct != null) { - return ( - - {" "} - {scan.active_completion_pct}% - - ); - } - - const { icon, colorClass } = getStatusDisplay(scan); - return ; - }, - textValue: () => null, - }, - scan_name: { - id: "scan_name", - accessorKey: "scan_name", - header: "Name", - headerTitle: COLUMN_HEADER_TITLES.scan_name, - size: 120, - minSize: 80, - maxSize: 300, - meta: { - filterable: true, - filterType: "string", - }, - }, - transcript_count: { - id: "transcript_count", - accessorKey: "transcript_count", - header: "Transcripts", - headerTitle: COLUMN_HEADER_TITLES.transcript_count, - size: 100, - minSize: 60, - maxSize: 200, - meta: { - filterable: true, - filterType: "number", - }, - cell: (info) => { - const value = info.getValue() as number; - return formatNumber(value); - }, - textValue: (value) => { - return formatNumber(value as number); - }, - }, - scanners: { - id: "scanners", - accessorKey: "scanners", - header: "Scanners", - headerTitle: COLUMN_HEADER_TITLES.scanners, - size: 120, - minSize: 80, - maxSize: 400, - meta: { - filterable: true, - filterType: "string", - }, - }, - scan_id: { - id: "scan_id", - accessorKey: "scan_id", - header: "Scan ID", - headerTitle: COLUMN_HEADER_TITLES.scan_id, - size: 150, - minSize: 100, - maxSize: 400, - meta: { - filterable: true, - filterType: "string", - }, - }, - model: { - id: "model", - accessorKey: "model", - header: "Model", - headerTitle: COLUMN_HEADER_TITLES.model, - size: 150, - minSize: 80, - maxSize: 400, - meta: { - filterable: true, - filterType: "string", - }, - }, - location: { - id: "location", - accessorKey: "relativeLocation", - header: "Path", - headerTitle: COLUMN_HEADER_TITLES.location, - size: 200, - minSize: 100, - maxSize: 500, - meta: { - filterable: true, - filterType: "string", - }, - }, - timestamp: { - id: "timestamp", - accessorKey: "timestamp", - header: "Time", - headerTitle: COLUMN_HEADER_TITLES.timestamp, - size: 180, - minSize: 120, - maxSize: 300, - meta: { - filterable: true, - filterType: "datetime", - }, - cell: (info) => { - const timestamp = info.getValue() as string; - if (!timestamp) return "-"; - return new Date(timestamp).toLocaleString(); - }, - textValue: (value) => { - if (!value) return "-"; - return new Date(value as string).toLocaleString(); - }, - }, - - // ============================================ - // New columns from spec - // ============================================ - scan_file: { - id: "scan_file", - accessorKey: "scan_file", - header: "Scan File", - headerTitle: COLUMN_HEADER_TITLES.scan_file, - size: 200, - minSize: 100, - maxSize: 500, - meta: { - filterable: true, - filterType: "string", - }, - cell: (info) => { - const value = info.getValue() as string | null; - return value || "-"; - }, - }, - tags: { - id: "tags", - accessorKey: "tags", - header: "Tags", - headerTitle: COLUMN_HEADER_TITLES.tags, - size: 150, - minSize: 80, - maxSize: 400, - meta: { - filterable: true, - filterType: "string", - }, - cell: (info) => { - const value = info.getValue() as string; - return value || "-"; - }, - }, - revision_version: { - id: "revision_version", - accessorKey: "revision_version", - header: "Version", - headerTitle: COLUMN_HEADER_TITLES.revision_version, - size: 150, - minSize: 80, - maxSize: 300, - meta: { - filterable: true, - filterType: "string", - }, - cell: (info) => { - const value = info.getValue() as string | null; - return value || "-"; - }, - }, - revision_commit: { - id: "revision_commit", - accessorKey: "revision_commit", - header: "Commit", - headerTitle: COLUMN_HEADER_TITLES.revision_commit, - size: 120, - minSize: 80, - maxSize: 300, - meta: { - filterable: true, - filterType: "string", - }, - cell: (info) => { - const value = info.getValue() as string | null; - if (!value) return "-"; - // Show first 7 characters of commit hash (standard git short hash) - return value.slice(0, 7); - }, - textValue: (value) => { - if (!value) return "-"; - const strValue = value as string; - return strValue.slice(0, 7); - }, - }, - revision_origin: { - id: "revision_origin", - accessorKey: "revision_origin", - header: "Origin", - headerTitle: COLUMN_HEADER_TITLES.revision_origin, - size: 200, - minSize: 100, - maxSize: 500, - meta: { - filterable: true, - filterType: "string", - }, - cell: (info) => { - const value = info.getValue() as string | null; - return value || "-"; - }, - }, - packages: { - id: "packages", - accessorKey: "packages", - header: "Packages", - headerTitle: COLUMN_HEADER_TITLES.packages, - size: 200, - minSize: 80, - maxSize: 500, - meta: { - filterable: true, - filterType: "string", - }, - cell: (info) => { - const value = info.getValue() as Record | undefined; - return formatObjectValue(value); - }, - textValue: (value) => { - return formatObjectValue(value as Record | undefined); - }, - titleValue: (value) => { - return getObjectTitleValue(value as Record | undefined); - }, - }, - metadata: { - id: "metadata", - accessorKey: "metadata", - header: "Metadata", - headerTitle: COLUMN_HEADER_TITLES.metadata, - size: 200, - minSize: 80, - maxSize: 500, - meta: { - filterable: true, - filterType: "string", - }, - cell: (info) => { - const value = info.getValue() as Record | undefined; - return formatObjectValue(value); - }, - textValue: (value) => { - return formatObjectValue(value as Record | undefined); - }, - titleValue: (value) => { - return getObjectTitleValue(value as Record | undefined); - }, - }, - scan_args: { - id: "scan_args", - accessorKey: "scan_args", - header: "Scan Args", - headerTitle: COLUMN_HEADER_TITLES.scan_args, - size: 200, - minSize: 80, - maxSize: 500, - meta: { - filterable: true, - filterType: "string", - }, - cell: (info) => { - const value = info.getValue() as Record | undefined; - return formatObjectValue(value); - }, - textValue: (value) => { - return formatObjectValue(value as Record | undefined); - }, - titleValue: (value) => { - return getObjectTitleValue(value as Record | undefined); - }, - }, - - // ============================================ - // Aggregated summary columns - // ============================================ - total_results: { - id: "total_results", - accessorKey: "total_results", - header: "Results", - headerTitle: COLUMN_HEADER_TITLES.total_results, - size: 100, - minSize: 60, - maxSize: 200, - meta: { - filterable: true, - filterType: "number", - }, - cell: (info) => { - const value = info.getValue() as number; - return formatNumber(value); - }, - textValue: (value) => { - return formatNumber(value as number); - }, - }, - total_errors: { - id: "total_errors", - accessorKey: "total_errors", - header: "Scanner Errors", - headerTitle: COLUMN_HEADER_TITLES.total_errors, - size: 100, - minSize: 60, - maxSize: 200, - meta: { - filterable: true, - filterType: "number", - }, - cell: (info) => { - const value = info.getValue() as number; - return formatNumber(value); - }, - textValue: (value) => { - return formatNumber(value as number); - }, - }, - total_tokens: { - id: "total_tokens", - accessorKey: "total_tokens", - header: "Tokens", - headerTitle: COLUMN_HEADER_TITLES.total_tokens, - size: 120, - minSize: 60, - maxSize: 200, - meta: { - filterable: true, - filterType: "number", - }, - cell: (info) => { - const value = info.getValue() as number; - return formatNumber(value); - }, - textValue: (value) => { - return formatNumber(value as number); - }, - }, -}; - -// Default column order - includes all columns -export const DEFAULT_COLUMN_ORDER: ScanColumnKey[] = [ - // Original columns first - "status", - "scan_name", - "scanners", - "scan_id", - "model", - "location", - "timestamp", - // Aggregated summary columns - "transcript_count", - "total_results", - "total_errors", - "total_tokens", - // New columns from spec - "scan_file", - "tags", - "revision_version", - "revision_commit", - "revision_origin", - "packages", - "metadata", - "scan_args", -]; - -// Default visible columns - the original 7 plus total_results -export const DEFAULT_VISIBLE_COLUMNS: ScanColumnKey[] = [ - "status", - "scan_name", - "scanners", - "scan_id", - "model", - "location", - "timestamp", - "transcript_count", - "total_results", -]; - -/** - * Get columns for the ScansGrid. - * @param visibleColumnKeys - Optional list of column keys to display. If not provided, returns default visible columns. - * @returns Array of column definitions in the order specified or default visible columns. - */ -export function getScanColumns( - visibleColumnKeys?: ScanColumnKey[] -): ScanColumn[] { - if (!visibleColumnKeys) { - return DEFAULT_VISIBLE_COLUMNS.map((key) => ALL_COLUMNS[key]); - } - - return visibleColumnKeys.map((key) => ALL_COLUMNS[key]); -} - -// Columns available for filtering (used by Add Filter popover) -export const FILTER_COLUMNS: AvailableColumn[] = DEFAULT_COLUMN_ORDER.map( - (columnId) => ({ - id: columnId, - label: COLUMN_LABELS[columnId], - filterType: ALL_COLUMNS[columnId].meta?.filterType ?? "string", - }) -); diff --git a/src/inspect_scout/_view/www/src/app/scans/constants.ts b/src/inspect_scout/_view/www/src/app/scans/constants.ts deleted file mode 100644 index 58d138310..000000000 --- a/src/inspect_scout/_view/www/src/app/scans/constants.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Infinite Scroll Tuning - * - * Goal: user never hits bottom while waiting for next page. - * - * Formula: threshold >= scroll_speed × fetch_duration - * - * Assumptions: - * row_height = 29px - * fetch_duration = 300-1000ms (variable with fixed overhead) - * max_scroll_speed = 1500px/s (typical fast scroller) - * - * Check at typical speed (1500px/s): - * runway_time = 2000px / 1500px/s = 1333ms - * worst_case_fetch = 1000ms - * margin = 333ms ✓ - * - * Check at extreme speed (5000px/s): - * runway_time = 2000px / 5000px/s = 400ms - * median_fetch = ~350ms - * margin = 50ms (tight but ok) ✓ - * - * Why large pageSize? Fetch duration is mostly fixed overhead, so larger - * pages = fewer fetches = fewer stall opportunities. 500 rows gives ~9.7s - * of scrolling per page at 1500px/s. - * - * Note: If threshold > pageSize_px, the next page is prefetched immediately - * after the current page loads. This is fine for maximum smoothness. - */ -export const SCANS_INFINITE_SCROLL_CONFIG = { - /** Number of rows to fetch per page (500 rows = 14,500px at 29px/row) */ - pageSize: 500, - /** Distance from bottom (in px) at which to trigger fetch (~69 rows) */ - threshold: 2000, -} as const; diff --git a/src/inspect_scout/_view/www/src/app/server/index.ts b/src/inspect_scout/_view/www/src/app/server/index.ts deleted file mode 100644 index 2777ae8f3..000000000 --- a/src/inspect_scout/_view/www/src/app/server/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { SortingState } from "@tanstack/react-table"; - -import { OrderByModel } from "../../query"; - -export const sortingStateToOrderBy = (sorting: SortingState): OrderByModel[] => - sorting.map((s) => ({ column: s.id, direction: s.desc ? "DESC" : "ASC" })); - -export type CursorType = { [key: string]: unknown }; diff --git a/src/inspect_scout/_view/www/src/app/server/useActiveScan.test.ts b/src/inspect_scout/_view/www/src/app/server/useActiveScan.test.ts deleted file mode 100644 index 95ef41182..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useActiveScan.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -// @vitest-environment jsdom -import { renderHook, waitFor } from "@testing-library/react"; -import { http, HttpResponse } from "msw"; -import { describe, expect, it } from "vitest"; - -import { createActiveScanInfo } from "../../test/objectFactories"; -import { server } from "../../test/setup-msw"; -import { createTestWrapper } from "../../test/test-utils"; -import type { ActiveScansResponse } from "../../types/api-types"; - -import { useActiveScan } from "./useActiveScan"; - -const scan123 = createActiveScanInfo({ - scan_id: "scan-123", - total_scans: 10, - metrics: { - batch_failures: 0, - batch_pending: 0, - buffered_scanner_jobs: 0, - completed_scans: 5, - memory_usage: 0, - process_count: 0, - task_count: 0, - tasks_idle: 0, - tasks_parsing: 0, - tasks_scanning: 0, - }, -}); - -const scan456 = createActiveScanInfo({ - scan_id: "scan-456", - total_scans: 20, - metrics: { - batch_failures: 0, - batch_pending: 0, - buffered_scanner_jobs: 0, - completed_scans: 15, - memory_usage: 0, - process_count: 0, - task_count: 0, - tasks_idle: 0, - tasks_parsing: 0, - tasks_scanning: 0, - }, -}); - -const activeScanResponse: ActiveScansResponse = { - items: { - "scan-123": scan123, - "scan-456": scan456, - }, -}; - -describe("useActiveScan", () => { - it("returns matching scan info when scan is active", async () => { - server.use( - http.get("/api/v2/scans/active", () => - HttpResponse.json(activeScanResponse) - ) - ); - - const { result } = renderHook(() => useActiveScan("scan-123"), { - wrapper: createTestWrapper(), - }); - - expect(result.current.loading).toBe(true); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toEqual(scan123); - }); - - it("returns undefined when scan is not found", async () => { - server.use( - http.get("/api/v2/scans/active", () => - HttpResponse.json(activeScanResponse) - ) - ); - - const { result } = renderHook(() => useActiveScan("nonexistent"), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toBeUndefined(); - }); - - it("returns undefined when scanId is undefined", async () => { - server.use( - http.get("/api/v2/scans/active", () => - HttpResponse.json(activeScanResponse) - ) - ); - - const { result } = renderHook(() => useActiveScan(undefined), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toBeUndefined(); - }); - - it("returns error when fetch fails", async () => { - server.use( - http.get("/api/v2/scans/active", () => - HttpResponse.text("Server Error", { status: 500 }) - ) - ); - - const { result } = renderHook(() => useActiveScan("scan-123"), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeDefined(); - expect(result.current.error?.message).toContain("500"); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/server/useActiveScan.ts b/src/inspect_scout/_view/www/src/app/server/useActiveScan.ts deleted file mode 100644 index c35d59a84..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useActiveScan.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useCallback } from "react"; - -import { useMapAsyncData } from "../../hooks/useMapAsyncData"; -import { ActiveScanInfo } from "../../types/api-types"; -import { AsyncData } from "../../utils/asyncData"; - -import { useActiveScans } from "./useActiveScans"; - -export const useActiveScan = ( - scanId: string | undefined -): AsyncData => - useMapAsyncData( - useActiveScans(), - useCallback( - (activeScans: Record) => - scanId ? (activeScans[scanId] ?? undefined) : undefined, - [scanId] - ) - ); diff --git a/src/inspect_scout/_view/www/src/app/server/useActiveScans.test.ts b/src/inspect_scout/_view/www/src/app/server/useActiveScans.test.ts deleted file mode 100644 index 7a2e1c14a..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useActiveScans.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -// @vitest-environment jsdom -import { renderHook, waitFor } from "@testing-library/react"; -import { http, HttpResponse } from "msw"; -import { describe, expect, it } from "vitest"; - -import { createActiveScanInfo } from "../../test/objectFactories"; -import { server } from "../../test/setup-msw"; -import { createTestWrapper } from "../../test/test-utils"; -import type { ActiveScansResponse } from "../../types/api-types"; - -import { useActiveScans } from "./useActiveScans"; - -describe("useActiveScans", () => { - it("returns loading then data on successful fetch", async () => { - const scanInfo = createActiveScanInfo({ - scan_id: "scan-123", - metrics: { - batch_failures: 0, - batch_pending: 0, - buffered_scanner_jobs: 0, - completed_scans: 100, - memory_usage: 0, - process_count: 2, - task_count: 4, - tasks_idle: 0, - tasks_parsing: 0, - tasks_scanning: 0, - }, - }); - - server.use( - http.get("/api/v2/scans/active", () => - HttpResponse.json({ - items: { "scan-123": scanInfo }, - }) - ) - ); - - const { result } = renderHook(() => useActiveScans(), { - wrapper: createTestWrapper(), - }); - - expect(result.current.loading).toBe(true); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toEqual({ "scan-123": scanInfo }); - }); - - it("handles empty active scans", async () => { - server.use( - http.get("/api/v2/scans/active", () => - HttpResponse.json({ items: {} }) - ) - ); - - const { result } = renderHook(() => useActiveScans(), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toEqual({}); - }); - - it("returns error on server failure", async () => { - server.use( - http.get("/api/v2/scans/active", () => - HttpResponse.text("Internal Server Error", { status: 500 }) - ) - ); - - const { result } = renderHook(() => useActiveScans(), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeDefined(); - expect(result.current.error?.message).toContain("500"); - }); - - it("sends GET request to correct endpoint", async () => { - let capturedMethod: string | undefined; - let capturedUrl: string | undefined; - - server.use( - http.get("/api/v2/scans/active", ({ request }) => { - capturedMethod = request.method; - capturedUrl = request.url; - return HttpResponse.json({ - items: {}, - }); - }) - ); - - const { result } = renderHook(() => useActiveScans(), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(capturedMethod).toBe("GET"); - expect(capturedUrl).toContain("/api/v2/scans/active"); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/server/useActiveScans.ts b/src/inspect_scout/_view/www/src/app/server/useActiveScans.ts deleted file mode 100644 index 4596a1198..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useActiveScans.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useApi } from "../../state/store"; -import { ActiveScanInfo } from "../../types/api-types"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -export const useActiveScans = (): AsyncData> => { - const api = useApi(); - return useAsyncDataFromQuery({ - queryKey: ["active-scans"], - queryFn: async () => (await api.getActiveScans()).items, - refetchInterval: 5000, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useAdjacentTranscriptIds.test.ts b/src/inspect_scout/_view/www/src/app/server/useAdjacentTranscriptIds.test.ts deleted file mode 100644 index f8bb68654..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useAdjacentTranscriptIds.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -// @vitest-environment jsdom -import { renderHook, waitFor } from "@testing-library/react"; -import { http, HttpResponse } from "msw"; -import { describe, expect, it } from "vitest"; - -import { server } from "../../test/setup-msw"; -import { createTestWrapper } from "../../test/test-utils"; -import type { - TranscriptInfo, - TranscriptsResponse, -} from "../../types/api-types"; -import { encodeBase64Url } from "../../utils/base64url"; - -import { useAdjacentTranscriptIds } from "./useAdjacentTranscriptIds"; - -const location = "/test-transcripts"; -const encodedLocation = encodeBase64Url(location); -const endpoint = `/api/v2/transcripts/${encodedLocation}`; - -const makeTranscript = (id: string): TranscriptInfo => ({ - transcript_id: id, - metadata: {}, -}); - -type CursorObject = { page: string } | null; - -type PaginationBody = { - filter: unknown; - order_by: unknown; - pagination: { cursor: CursorObject } | null; -}; - -/** - * Creates an MSW handler that returns paginated transcript responses. - * Each entry in `pages` maps a cursor key to a page of transcript IDs. - * The first entry (cursor `null`) is the initial page. - */ -const handlePaginatedTranscripts = ( - pages: Map -) => - http.post(endpoint, async ({ request }) => { - const body = (await request.json()) as PaginationBody; - const cursorObj = body.pagination?.cursor ?? null; - const cursorKey = cursorObj?.page ?? null; - const page = pages.get(cursorKey); - if (!page) { - return HttpResponse.json({ - items: [], - next_cursor: null, - total_count: 0, - }); - } - return HttpResponse.json({ - items: page.ids.map(makeTranscript), - next_cursor: page.nextCursor ? { page: page.nextCursor } : null, - total_count: page.ids.length, - }); - }); - -describe("useAdjacentTranscriptIds", () => { - it("returns [undefined, nextId] for the first item", async () => { - server.use( - handlePaginatedTranscripts( - new Map([[null, { ids: ["id-1", "id-2", "id-3"], nextCursor: null }]]) - ) - ); - - const { result } = renderHook( - () => useAdjacentTranscriptIds("id-1", location), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toEqual([undefined, "id-2"]); - }); - - it("returns [prevId, nextId] for a middle item", async () => { - server.use( - handlePaginatedTranscripts( - new Map([[null, { ids: ["id-1", "id-2", "id-3"], nextCursor: null }]]) - ) - ); - - const { result } = renderHook( - () => useAdjacentTranscriptIds("id-2", location), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toEqual(["id-1", "id-3"]); - }); - - it("returns [prevId, undefined] for the last item with no more pages", async () => { - server.use( - handlePaginatedTranscripts( - new Map([[null, { ids: ["id-1", "id-2", "id-3"], nextCursor: null }]]) - ) - ); - - const { result } = renderHook( - () => useAdjacentTranscriptIds("id-3", location), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toEqual(["id-2", undefined]); - }); - - it("fetches next page when viewing last item with more pages available", async () => { - server.use( - handlePaginatedTranscripts( - new Map([ - [null, { ids: ["id-1", "id-2"], nextCursor: "cursor-2" }], - ["cursor-2", { ids: ["id-3", "id-4"], nextCursor: null }], - ]) - ) - ); - - const { result } = renderHook( - () => useAdjacentTranscriptIds("id-2", location), - { wrapper: createTestWrapper() } - ); - - // Initially loads first page, id-2 is last item so it triggers fetchNextPage - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - // After second page loads, id-3 becomes the next adjacent ID - await waitFor(() => { - expect(result.current.data).toEqual(["id-1", "id-3"]); - }); - }); - - it("returns adjacent ids across page boundaries", async () => { - const wrapper = createTestWrapper(); - - server.use( - handlePaginatedTranscripts( - new Map([ - [null, { ids: ["id-1", "id-2"], nextCursor: "cursor-2" }], - ["cursor-2", { ids: ["id-3", "id-4"], nextCursor: null }], - ]) - ) - ); - - // First, view id-2 (last item on page 1) — this triggers fetchNextPage - const { result: result1 } = renderHook( - () => useAdjacentTranscriptIds("id-2", location, 2), - { wrapper } - ); - - // Wait for both pages to load (id-2 triggers page 2 fetch) - await waitFor(() => { - expect(result1.current.loading).toBe(false); - expect(result1.current.data).toEqual(["id-1", "id-3"]); - }); - - // Now view id-3 (first item on page 2) — both pages are cached - const { result: result2 } = renderHook( - () => useAdjacentTranscriptIds("id-3", location, 2), - { wrapper } - ); - - await waitFor(() => { - expect(result2.current.loading).toBe(false); - }); - - // id-2 is last on prev page, id-4 is next on same page - expect(result2.current.data).toEqual(["id-2", "id-4"]); - }); - - it("returns error when fetch fails", async () => { - server.use( - http.post(endpoint, () => - HttpResponse.text("Server Error", { status: 500 }) - ) - ); - - const { result } = renderHook( - () => useAdjacentTranscriptIds("id-1", location), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeDefined(); - expect(result.current.error?.message).toContain("500"); - }); - - it("returns loading when location is null", async () => { - const { result } = renderHook( - () => useAdjacentTranscriptIds("id-1", null), - { wrapper: createTestWrapper() } - ); - - // No fetch should be made — skipToken path - expect(result.current.loading).toBe(true); - - await new Promise((r) => setTimeout(r, 50)); - expect(result.current.loading).toBe(true); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/server/useAdjacentTranscriptIds.ts b/src/inspect_scout/_view/www/src/app/server/useAdjacentTranscriptIds.ts deleted file mode 100644 index 4a3ccfc3b..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useAdjacentTranscriptIds.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; -import { SortingState } from "@tanstack/react-table"; -import { useEffect, useMemo } from "react"; - -import { Condition } from "../../query"; -import { TranscriptsResponse } from "../../types/api-types"; -import { AsyncData, data, loading } from "../../utils/asyncData"; - -import { useServerTranscriptsInfinite } from "./useServerTranscriptsInfinite"; - -type Position = { pageIndex: number; itemIndex: number }; - -/** - * Returns prev/next transcript IDs relative to the given ID. - * - * Note: prev is undefined when viewing the first loaded item. - * Reverse paging not yet supported - will need backend prev_cursor. - */ -export const useAdjacentTranscriptIds = ( - id: string, - location?: string | null, - pageSize?: number, - filter?: Condition, - sorting?: SortingState -): AsyncData<[string | undefined, string | undefined]> => { - const { - data: queryData, - error, - isLoading, - hasNextPage, - isFetchingNextPage, - fetchNextPage, - } = useServerTranscriptsInfinite( - location ? { location, pageSize, filter, sorting } : skipToken - ); - - const pages = queryData?.pages; - const position = useMemo( - () => (pages ? findPosition(pages, id) : null), - [pages, id] - ); - - const needsNextPage = - pages !== undefined && - position !== null && - isLastLoadedItem(pages, position) && - hasNextPage && - !isFetchingNextPage; - - useEffect(() => { - if (needsNextPage) { - fetchNextPage().catch(console.error); - } - }, [needsNextPage, fetchNextPage]); - - if (isLoading) { - return loading; - } - - if (error) { - return { loading: false, error }; - } - - if (pages === undefined || position === null) { - // ID not found in loaded pages - might need more data - return loading; - } - - return data(getAdjacentIds(pages, position)); -}; - -const findPosition = ( - pages: TranscriptsResponse[], - id: string -): Position | null => { - for (let p = 0; p < pages.length; p++) { - const page = pages[p]; - if (!page) continue; - const i = page.items.findIndex((item) => item.transcript_id === id); - if (i >= 0) return { pageIndex: p, itemIndex: i }; - } - return null; -}; - -const getAdjacentIds = ( - pages: TranscriptsResponse[], - pos: Position -): [string | undefined, string | undefined] => { - const page = pages[pos.pageIndex]!; - // Note: prevId is undefined at position 0; reverse paging not yet supported - const prevId = - pos.itemIndex > 0 - ? page.items[pos.itemIndex - 1]?.transcript_id - : pages[pos.pageIndex - 1]?.items.at(-1)?.transcript_id; - const nextId = - pos.itemIndex < page.items.length - 1 - ? page.items[pos.itemIndex + 1]?.transcript_id - : pages[pos.pageIndex + 1]?.items[0]?.transcript_id; - return [prevId, nextId]; -}; - -const isLastLoadedItem = ( - pages: TranscriptsResponse[], - pos: Position -): boolean => - pos.pageIndex === pages.length - 1 && - pos.itemIndex === pages[pos.pageIndex]!.items.length - 1; diff --git a/src/inspect_scout/_view/www/src/app/server/useAppConfig.ts b/src/inspect_scout/_view/www/src/app/server/useAppConfig.ts deleted file mode 100644 index 4fb31ee24..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useAppConfig.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { useApi } from "../../state/store"; -import { AppConfig } from "../../types/api-types"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -/** - * Loads app config asynchronously at app initialization. - * - * Use this hook only at the top of the app before rendering to load config - * data globally. After this completes, all other components should use - * useConfig to access the loaded value synchronously. - */ -export const useAppConfigAsync = (): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: ["config", "project-config-inv"], - queryFn: () => api.getConfig(), - staleTime: Infinity, - }); -}; - -/** - * Returns app config for use in components after data loaded globally. - * - * Use this hook in regular components throughout the app. Assumes the async - * data has already been loaded at app initialization via useConfigAsync. - * Throws if data not yet available. - */ -export const useAppConfig = (): AppConfig => { - const { data } = useAppConfigAsync(); - if (!data) throw new Error("App Config not loaded"); - return data; -}; - -export function appAliasedPath( - appConfig: AppConfig, - path: string | null -): string | null { - if (path == null) { - return null; - } - return path.replace(appConfig.home_dir, "~"); -} - -/** Strips leading slash from path if present */ -function stripLeadingSlash(path: string): string { - return path.startsWith("/") ? path.slice(1) : path; -} - -export function projectOrAppAliasedPath( - appConfig: AppConfig, - path: string | null -): string | null { - if (path == null) { - return null; - } - - // Prefer project-relative path when within project directory - if (appConfig.project_dir && path.startsWith(appConfig.project_dir)) { - return stripLeadingSlash(path.slice(appConfig.project_dir.length)); - } - - // Fall back to home-aliased path - return appAliasedPath(appConfig, path); -} diff --git a/src/inspect_scout/_view/www/src/app/server/useCode.test.ts b/src/inspect_scout/_view/www/src/app/server/useCode.test.ts deleted file mode 100644 index b879a8247..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useCode.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -// @vitest-environment jsdom -import { skipToken } from "@tanstack/react-query"; -import { renderHook, waitFor } from "@testing-library/react"; -import { http, HttpResponse } from "msw"; -import { describe, expect, it } from "vitest"; - -import { Column } from "../../query/column"; -import { server } from "../../test/setup-msw"; -import { createTestWrapper } from "../../test/test-utils"; - -import { useCode } from "./useCode"; - -const simpleCondition = new Column("total_tokens").lt(75); - -describe("useCode", () => { - it("returns loading then data on successful fetch", async () => { - const mockResponse = { - python: "Not Yet Implemented", - sqlite: '"total_tokens" < ?', - duckdb: '"total_tokens" < ?', - postgres: '"total_tokens" < $1', - }; - - server.use( - http.post("/api/v2/code", () => - HttpResponse.json>(mockResponse) - ) - ); - - const { result } = renderHook(() => useCode(simpleCondition), { - wrapper: createTestWrapper(), - }); - - expect(result.current.loading).toBe(true); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toEqual(mockResponse); - }); - - it("sends condition as request body", async () => { - let capturedBody: unknown; - - server.use( - http.post("/api/v2/code", async ({ request }) => { - capturedBody = await request.json(); - return HttpResponse.json>({ python: "test" }); - }) - ); - - const { result } = renderHook(() => useCode(simpleCondition), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - // Condition is serialized via JSON.stringify, so compare as JSON - expect(capturedBody).toEqual(JSON.parse(JSON.stringify(simpleCondition))); - }); - - it("returns error on server failure", async () => { - server.use( - http.post("/api/v2/code", () => - HttpResponse.text("Bad Request", { status: 400 }) - ) - ); - - const { result } = renderHook(() => useCode(simpleCondition), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeDefined(); - expect(result.current.error?.message).toContain("400"); - }); - - it("does not make request when skipToken is passed", async () => { - let requestMade = false; - - server.use( - http.post("/api/v2/code", () => { - requestMade = true; - return HttpResponse.json>({}); - }) - ); - - const { result } = renderHook(() => useCode(skipToken), { - wrapper: createTestWrapper(), - }); - - // With skipToken, query stays pending and no request is made - expect(result.current.loading).toBe(true); - - // Give it a tick to ensure no request fires - await new Promise((r) => setTimeout(r, 50)); - expect(requestMade).toBe(false); - }); - - it("uses different cache entries for different conditions", async () => { - let requestCount = 0; - - server.use( - http.post("/api/v2/code", () => { - requestCount++; - return HttpResponse.json>({ - python: `response-${requestCount}`, - }); - }) - ); - - const wrapper = createTestWrapper(); - - const { result: result1 } = renderHook( - () => useCode(new Column("a").eq(1)), - { wrapper } - ); - const { result: result2 } = renderHook( - () => useCode(new Column("b").eq(2)), - { wrapper } - ); - - await waitFor(() => { - expect(result1.current.loading).toBe(false); - expect(result2.current.loading).toBe(false); - }); - - // Two different conditions should trigger two separate requests - expect(requestCount).toBe(2); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/server/useCode.ts b/src/inspect_scout/_view/www/src/app/server/useCode.ts deleted file mode 100644 index 586cd8752..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useCode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; - -import { Condition } from "../../query/types"; -import { useApi } from "../../state/store"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -export const useCode = ( - condition: Condition | typeof skipToken -): AsyncData> => { - const api = useApi(); - return useAsyncDataFromQuery({ - queryKey: ["code", condition], - queryFn: - condition === skipToken ? skipToken : () => api.postCode(condition), - staleTime: Infinity, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useHasTranscript.ts b/src/inspect_scout/_view/www/src/app/server/useHasTranscript.ts deleted file mode 100644 index 581e61f9f..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useHasTranscript.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; - -import { useApi } from "../../state/store"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -type HasTranscriptParams = { - location: string; - id: string; -}; - -export const useHasTranscript = ( - params: HasTranscriptParams | typeof skipToken -): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: params === skipToken ? [skipToken] : ["has_transcript", params], - queryFn: - params === skipToken - ? skipToken - : () => api.hasTranscript(params.location, params.id), - staleTime: Infinity, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useProjectConfig.test.ts b/src/inspect_scout/_view/www/src/app/server/useProjectConfig.test.ts deleted file mode 100644 index 113dbe665..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useProjectConfig.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -// @vitest-environment jsdom -import { renderHook, waitFor, act } from "@testing-library/react"; -import { http, HttpResponse } from "msw"; -import { describe, expect, it } from "vitest"; - -import { server } from "../../test/setup-msw"; -import { createTestWrapper } from "../../test/test-utils"; -import type { ProjectConfig } from "../../types/api-types"; - -import { useProjectConfig, useUpdateProjectConfig } from "./useProjectConfig"; - -const mockConfig = { - filter: ["task_id = 'test'"], -}; - -describe("useProjectConfig", () => { - it("returns config with etag from response headers", async () => { - server.use( - http.get("/api/v2/project/config", () => - HttpResponse.json(mockConfig, { - headers: { ETag: '"abc123"' }, - }) - ) - ); - - const { result } = renderHook(() => useProjectConfig(), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toEqual({ - config: mockConfig, - etag: "abc123", - }); - }); - - it("returns empty etag when header is missing", async () => { - server.use( - http.get("/api/v2/project/config", () => - HttpResponse.json(mockConfig) - ) - ); - - const { result } = renderHook(() => useProjectConfig(), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data?.etag).toBe(""); - }); - - it("returns error on server failure", async () => { - server.use( - http.get("/api/v2/project/config", () => - HttpResponse.text("Server Error", { status: 500 }) - ) - ); - - const { result } = renderHook(() => useProjectConfig(), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeDefined(); - }); -}); - -describe("useUpdateProjectConfig", () => { - it("sends config with If-Match header and returns updated config", async () => { - let capturedHeaders: Headers | undefined; - let capturedBody: unknown; - - const updatedConfig = { filter: ["updated"] }; - - server.use( - http.get("/api/v2/project/config", () => - HttpResponse.json(mockConfig, { - headers: { ETag: '"original"' }, - }) - ), - http.put("/api/v2/project/config", async ({ request }) => { - capturedHeaders = new Headers(request.headers); - capturedBody = await request.json(); - return HttpResponse.json(updatedConfig, { - headers: { ETag: '"updated-etag"' }, - }); - }) - ); - - const wrapper = createTestWrapper(); - - const { result: configResult } = renderHook(() => useProjectConfig(), { - wrapper, - }); - const { result: mutationResult } = renderHook( - () => useUpdateProjectConfig(), - { wrapper } - ); - - await waitFor(() => { - expect(configResult.current.loading).toBe(false); - }); - - await act(() => - mutationResult.current.mutateAsync({ - config: updatedConfig, - etag: "original", - }) - ); - - await waitFor(() => { - expect(mutationResult.current.isSuccess).toBe(true); - }); - - expect(capturedHeaders?.get("If-Match")).toBe('"original"'); - expect(capturedBody).toEqual(updatedConfig); - }); - - it("updates cache with new config and etag on success", async () => { - const updatedConfig = { filter: ["updated"] }; - - server.use( - http.get("/api/v2/project/config", () => - HttpResponse.json(mockConfig, { - headers: { ETag: '"v1"' }, - }) - ), - http.put("/api/v2/project/config", () => - HttpResponse.json(updatedConfig, { - headers: { ETag: '"v2"' }, - }) - ) - ); - - const wrapper = createTestWrapper(); - - const { result: configResult } = renderHook(() => useProjectConfig(), { - wrapper, - }); - const { result: mutationResult } = renderHook( - () => useUpdateProjectConfig(), - { wrapper } - ); - - await waitFor(() => { - expect(configResult.current.loading).toBe(false); - }); - - await act(() => - mutationResult.current.mutateAsync({ config: updatedConfig, etag: "v1" }) - ); - - await waitFor(() => { - expect(mutationResult.current.isSuccess).toBe(true); - }); - - // The config query cache should be updated with the new data - await waitFor(() => { - expect(configResult.current.data).toEqual({ - config: updatedConfig, - etag: "v2", - }); - }); - }); - - it("propagates 412 Precondition Failed as error", async () => { - server.use( - http.get("/api/v2/project/config", () => - HttpResponse.json(mockConfig, { - headers: { ETag: '"v1"' }, - }) - ), - http.put("/api/v2/project/config", () => - HttpResponse.text("Precondition Failed", { status: 412 }) - ) - ); - - const wrapper = createTestWrapper(); - - const { result: configResult } = renderHook(() => useProjectConfig(), { - wrapper, - }); - const { result: mutationResult } = renderHook( - () => useUpdateProjectConfig(), - { wrapper } - ); - - await waitFor(() => { - expect(configResult.current.loading).toBe(false); - }); - - await act(async () => { - try { - await mutationResult.current.mutateAsync({ - config: mockConfig, - etag: "stale", - }); - } catch { - // expected - } - }); - - await waitFor(() => { - expect(mutationResult.current.isError).toBe(true); - }); - - expect(mutationResult.current.error?.message).toContain("412"); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/server/useProjectConfig.ts b/src/inspect_scout/_view/www/src/app/server/useProjectConfig.ts deleted file mode 100644 index 8ef881201..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useProjectConfig.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - DefaultError, - useMutation, - UseMutationResult, - useQueryClient, -} from "@tanstack/react-query"; - -import { useApi } from "../../state/store"; -import { ProjectConfig, ProjectConfigInput } from "../../types/api-types"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -export type ProjectConfigWithEtag = { - config: ProjectConfig; - etag: string; -}; - -/** - * Loads project configuration from scout.yaml. - * - * Returns both the config and an etag for optimistic concurrency control. - * - * Automatic refetching is disabled to support optimistic locking: - * - External changes are detected via etag mismatch on save (412 error) - * - User explicitly chooses to reload or force save on conflict - */ -export const useProjectConfig = (): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: ["project-config", "project-config-inv"], - queryFn: () => api.getProjectConfig(), - staleTime: Infinity, - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - }); -}; - -/** - * Mutation hook for updating project configuration. - * - * Updates scout.yaml while preserving comments and formatting. - * Requires the current etag for optimistic concurrency control. - * - * On success, updates the query cache with the new config and etag. - * On 412 Precondition Failed, the config was modified externally. - */ -export const useUpdateProjectConfig = (): UseMutationResult< - ProjectConfigWithEtag, - DefaultError, - { config: ProjectConfigInput; etag: string | null } -> => { - const api = useApi(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ config, etag }) => api.updateProjectConfig(config, etag), - onSuccess: (data) => { - // Update cache with new config and etag - queryClient.setQueryData(["project-config", "project-config-inv"], data); - queryClient - .invalidateQueries({ queryKey: ["config", "project-config-inv"] }) - .catch(console.log); - }, - }); -}; - -/* -import { useProjectConfig, useUpdateProjectConfig } from "./server/useProjectConfig"; - -function ConfigEditor() { - const configData = useProjectConfig(); - const mutation = useUpdateProjectConfig(); - - if (configData.loading) return ; - if (configData.error) return ; - - const { config, etag } = configData.data; - - const handleSave = async (newConfig: ProjectConfigInput) => { - try { - await mutation.mutateAsync({ config: newConfig, etag }); - } catch (error) { - if (error instanceof ApiError && error.status === 412) { - // Config was modified externally - prompt user to refresh - } - } - }; - return
; -} - -*/ diff --git a/src/inspect_scout/_view/www/src/app/server/useScan.ts b/src/inspect_scout/_view/www/src/app/server/useScan.ts deleted file mode 100644 index 1c3421c24..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useScan.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; - -import { useApi } from "../../state/store"; -import { Status } from "../../types/api-types"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -type ScanParams = { - scansDir: string; - scanPath: string; -}; - -// Fetches scan status from the server by location -export const useScan = ( - params: ScanParams | typeof skipToken -): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: - params === skipToken - ? [skipToken] - : ["scan", params.scansDir, params.scanPath, "scans-inv"], - queryFn: - params === skipToken - ? skipToken - : () => api.getScan(params.scansDir, params.scanPath), - staleTime: 10000, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useScanDataframe.ts b/src/inspect_scout/_view/www/src/app/server/useScanDataframe.ts deleted file mode 100644 index 019b4c1c1..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useScanDataframe.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; -import { ColumnTable } from "arquero"; - -import { useApi } from "../../state/store"; -import { decodeArrowBytes } from "../../utils/arrow"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; -import { expandResultsetRows } from "../utils/arrow"; - -type ScanDataframeParams = { - scansDir: string; - scanPath: string; - scanner: string; -}; - -// Fetches scanner dataframe from the server by location and scanner -export const useScanDataframe = ( - params: ScanDataframeParams | typeof skipToken -): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: - params === skipToken - ? [skipToken] - : [ - "scanDataframe", - params.scansDir, - params.scanPath, - params.scanner, - "scans-inv", - ], - queryFn: - params === skipToken - ? skipToken - : async () => - expandResultsetRows( - decodeArrowBytes( - await api.getScannerDataframe( - params.scansDir, - params.scanPath, - params.scanner - ) - ) - ), - staleTime: Infinity, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useScanDataframeInput.ts b/src/inspect_scout/_view/www/src/app/server/useScanDataframeInput.ts deleted file mode 100644 index 4484b76ea..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useScanDataframeInput.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; - -import { useApi } from "../../state/store"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; -import { ScanResultInputData } from "../types"; - -type ScanDataframeInputParams = { - scansDir: string; - scanPath: string; - scanner: string; - uuid: string; -}; - -export const useScanDataframeInput = ( - params: ScanDataframeInputParams | typeof skipToken -): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: - params === skipToken - ? [skipToken] - : ["scanDataframeInput", params, "scans-inv"], - queryFn: - params === skipToken - ? skipToken - : () => - api.getScannerDataframeInput( - params.scansDir, - params.scanPath, - params.scanner, - params.uuid - ), - staleTime: Infinity, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useScanners.ts b/src/inspect_scout/_view/www/src/app/server/useScanners.ts deleted file mode 100644 index e9aa20b7d..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useScanners.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useApi } from "../../state/store"; -import { ScannerInfo } from "../../types/api-types"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -export const useScanners = (): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: ["scanners"], - queryFn: async () => (await api.getScanners()).items, - staleTime: 10000, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useScans.ts b/src/inspect_scout/_view/www/src/app/server/useScans.ts deleted file mode 100644 index f26b68cc7..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useScans.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useApi } from "../../state/store"; -import { ScanRow } from "../../types/api-types"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -// Lists the available scans from the server and stores in state -export const useScans = (scansDir: string): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: ["scans", scansDir, "scans-inv"], - queryFn: async () => (await api.getScans(scansDir)).items, - staleTime: 5000, - refetchInterval: 5000, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useScansColumnValues.ts b/src/inspect_scout/_view/www/src/app/server/useScansColumnValues.ts deleted file mode 100644 index 8102963b5..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useScansColumnValues.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; - -import { ScalarValue } from "../../api/api"; -import { Condition } from "../../query"; -import { useApi } from "../../state/store"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -type ScansColumnValuesParams = { - location: string; - column: string; - filter: Condition | undefined; -}; - -export const useScansColumnValues = ( - params: ScansColumnValuesParams | typeof skipToken -): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: - params === skipToken - ? [skipToken] - : ["scansColumnValues", params, "scans-inv"], - queryFn: - params === skipToken - ? skipToken - : () => - api.getScansColumnValues( - params.location, - params.column, - params.filter - ), - staleTime: 10 * 60 * 1000, // We can be pretty liberal here - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useScansInfinite.ts b/src/inspect_scout/_view/www/src/app/server/useScansInfinite.ts deleted file mode 100644 index efa05d5b4..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useScansInfinite.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - UseInfiniteQueryResult, - InfiniteData, - useInfiniteQuery, - QueryKey, - keepPreviousData, -} from "@tanstack/react-query"; -import { SortingState } from "@tanstack/react-table"; -import { useMemo } from "react"; - -import { Condition } from "../../query"; -import { useApi } from "../../state/store"; -import { ScansResponse } from "../../types/api-types"; - -import { CursorType, sortingStateToOrderBy } from "."; - -export const useScansInfinite = ( - scansDir: string, - pageSize: number = 50, - filter?: Condition, - sorting?: SortingState -): UseInfiniteQueryResult< - InfiniteData, - Error -> => { - const api = useApi(); - - const orderBy = useMemo( - () => (sorting ? sortingStateToOrderBy(sorting) : undefined), - [sorting] - ); - - return useInfiniteQuery< - ScansResponse, - Error, - InfiniteData, - QueryKey, - CursorType | undefined - >({ - queryKey: [ - "scans-infinite", - scansDir, - filter, - orderBy, - pageSize, - "scans-inv", - ], - queryFn: async ({ pageParam }) => { - const pagination = pageParam - ? { limit: pageSize, cursor: pageParam, direction: "forward" as const } - : { limit: pageSize, cursor: null, direction: "forward" as const }; - - return await api.getScans(scansDir, filter, orderBy, pagination); - }, - initialPageParam: undefined, - getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined, - staleTime: 10000, - refetchInterval: 10000, - placeholderData: keepPreviousData, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useServerTranscripts.test.ts b/src/inspect_scout/_view/www/src/app/server/useServerTranscripts.test.ts deleted file mode 100644 index 0dffcee19..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useServerTranscripts.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -// @vitest-environment jsdom -import { renderHook, waitFor } from "@testing-library/react"; -import { http, HttpResponse } from "msw"; -import { describe, expect, it } from "vitest"; - -import { Column } from "../../query/column"; -import { server } from "../../test/setup-msw"; -import { createTestWrapper } from "../../test/test-utils"; -import type { TranscriptsResponse } from "../../types/api-types"; -import { encodeBase64Url } from "../../utils/base64url"; - -import { useServerTranscripts } from "./useServerTranscripts"; - -const location = "/test/transcripts"; -const encodedLocation = encodeBase64Url(location); -const endpoint = `/api/v2/transcripts/${encodedLocation}`; - -const mockTranscriptsResponse: TranscriptsResponse = { - items: [ - { transcript_id: "t-1", metadata: {} }, - { transcript_id: "t-2", metadata: {} }, - ], - next_cursor: null, - total_count: 2, -}; - -describe("useServerTranscripts", () => { - it("returns transcripts on successful fetch", async () => { - server.use( - http.post(endpoint, () => - HttpResponse.json(mockTranscriptsResponse) - ) - ); - - const { result } = renderHook(() => useServerTranscripts(location), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toEqual(mockTranscriptsResponse); - }); - - it("sends sorting state as order_by in request body", async () => { - let capturedBody: unknown; - - server.use( - http.post(endpoint, async ({ request }) => { - capturedBody = await request.json(); - return HttpResponse.json(mockTranscriptsResponse); - }) - ); - - const sorting = [ - { id: "date", desc: true }, - { id: "model", desc: false }, - ]; - - const { result } = renderHook( - () => useServerTranscripts(location, undefined, sorting), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(capturedBody).toEqual({ - filter: null, - order_by: [ - { column: "date", direction: "DESC" }, - { column: "model", direction: "ASC" }, - ], - pagination: null, - }); - }); - - it("sends filter in request body", async () => { - let capturedBody: unknown; - - server.use( - http.post(endpoint, async ({ request }) => { - capturedBody = await request.json(); - return HttpResponse.json(mockTranscriptsResponse); - }) - ); - - const filter = new Column("model").eq("gpt-4"); - - const { result } = renderHook( - () => useServerTranscripts(location, filter), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(capturedBody).toMatchObject({ - filter: JSON.parse(JSON.stringify(filter)), - }); - }); - - it("returns error on server failure", async () => { - server.use( - http.post(endpoint, () => - HttpResponse.text("Server Error", { status: 500 }) - ) - ); - - const { result } = renderHook(() => useServerTranscripts(location), { - wrapper: createTestWrapper(), - }); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeDefined(); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/server/useServerTranscripts.ts b/src/inspect_scout/_view/www/src/app/server/useServerTranscripts.ts deleted file mode 100644 index 52851e470..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useServerTranscripts.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { keepPreviousData } from "@tanstack/react-query"; -import { SortingState } from "@tanstack/react-table"; -import { useMemo } from "react"; - -import { Condition } from "../../query"; -import { useApi } from "../../state/store"; -import { TranscriptsResponse } from "../../types/api-types"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -import { sortingStateToOrderBy } from "."; - -export const useServerTranscripts = ( - location: string, - filter?: Condition, - sorting?: SortingState -): AsyncData => { - const api = useApi(); - - const orderBy = useMemo( - () => (sorting ? sortingStateToOrderBy(sorting) : undefined), - [sorting] - ); - - return useAsyncDataFromQuery({ - queryKey: ["transcripts", location, filter, orderBy], - queryFn: async () => await api.getTranscripts(location, filter, orderBy), - staleTime: 10 * 60 * 1000, - refetchInterval: 10 * 60 * 1000, - placeholderData: keepPreviousData, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useServerTranscriptsInfinite.ts b/src/inspect_scout/_view/www/src/app/server/useServerTranscriptsInfinite.ts deleted file mode 100644 index baea23ad7..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useServerTranscriptsInfinite.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - UseInfiniteQueryResult, - InfiniteData, - useInfiniteQuery, - QueryKey, - keepPreviousData, - skipToken, -} from "@tanstack/react-query"; -import { SortingState } from "@tanstack/react-table"; -import { useMemo } from "react"; - -import { Condition } from "../../query"; -import { useApi } from "../../state/store"; -import { TranscriptsResponse } from "../../types/api-types"; - -import { CursorType, sortingStateToOrderBy } from "."; - -type ServerTranscriptsInfiniteParams = { - location: string; - pageSize?: number; - filter?: Condition; - sorting?: SortingState; -}; - -export const useServerTranscriptsInfinite = ( - params: ServerTranscriptsInfiniteParams | typeof skipToken -): UseInfiniteQueryResult< - InfiniteData, - Error -> => { - const api = useApi(); - - const orderBy = useMemo( - () => - params !== skipToken && params.sorting - ? sortingStateToOrderBy(params.sorting) - : undefined, - [params] - ); - - const pageSize = params !== skipToken ? (params.pageSize ?? 50) : 50; - - return useInfiniteQuery< - TranscriptsResponse, - Error, - InfiniteData, - QueryKey, - CursorType | undefined - >({ - queryKey: - params === skipToken - ? [skipToken] - : [ - "transcripts-infinite", - params.location, - params.filter, - orderBy, - pageSize, - "project-config-inv", - ], - queryFn: - params === skipToken - ? skipToken - : async ({ pageParam }) => { - const pagination = pageParam - ? { - limit: pageSize, - cursor: pageParam, - direction: "forward" as const, - } - : { - limit: pageSize, - cursor: null, - direction: "forward" as const, - }; - - return await api.getTranscripts( - params.location, - params.filter, - orderBy, - pagination - ); - }, - initialPageParam: undefined, - getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined, - staleTime: 10 * 60 * 1000, - refetchInterval: 10 * 60 * 1000, - placeholderData: keepPreviousData, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useStartScan.test.ts b/src/inspect_scout/_view/www/src/app/server/useStartScan.test.ts deleted file mode 100644 index 76c40acda..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useStartScan.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -// @vitest-environment jsdom -import { renderHook, waitFor, act } from "@testing-library/react"; -import { http, HttpResponse } from "msw"; -import { describe, expect, it } from "vitest"; - -import { server } from "../../test/setup-msw"; -import { createTestWrapper } from "../../test/test-utils"; -import type { ScanJobConfig, Status } from "../../types/api-types"; - -import { useStartScan } from "./useStartScan"; - -const mockScanConfig: ScanJobConfig = { - filter: ["task_id = 'test'"], -}; - -const mockStatus: Status = { - complete: false, - errors: [], - location: "/scans/test", - spec: { - scan_id: "test-scan-id", - scan_name: "test-scan", - options: { max_transcripts: 25 }, - packages: {}, - scanners: {}, - timestamp: "2024-01-01T00:00:00Z", - }, - summary: { complete: false, scanners: {} }, -}; - -describe("useStartScan", () => { - it("sends scan config and returns status on success", async () => { - let capturedBody: unknown; - - server.use( - http.post("/api/v2/startscan", async ({ request }) => { - capturedBody = await request.json(); - return HttpResponse.json(mockStatus); - }) - ); - - const { result } = renderHook(() => useStartScan(), { - wrapper: createTestWrapper(), - }); - - await act(() => result.current.mutateAsync(mockScanConfig)); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(capturedBody).toEqual(mockScanConfig); - expect(result.current.data).toEqual(mockStatus); - }); - - it("sets error state on server failure", async () => { - server.use( - http.post("/api/v2/startscan", () => - HttpResponse.text("Bad config", { status: 400 }) - ) - ); - - const { result } = renderHook(() => useStartScan(), { - wrapper: createTestWrapper(), - }); - - await act(async () => { - try { - await result.current.mutateAsync(mockScanConfig); - } catch { - // expected - } - }); - - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); - - expect(result.current.error?.message).toContain("400"); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/server/useStartScan.ts b/src/inspect_scout/_view/www/src/app/server/useStartScan.ts deleted file mode 100644 index 5424554dc..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useStartScan.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - DefaultError, - useMutation, - UseMutationResult, -} from "@tanstack/react-query"; - -import { useApi } from "../../state/store"; -import { ScanJobConfig, Status } from "../../types/api-types"; - -export const useStartScan = (): UseMutationResult< - Status, - DefaultError, - ScanJobConfig -> => { - const api = useApi(); - return useMutation({ mutationFn: (config) => api.startScan(config) }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useTopicInvalidation.test.ts b/src/inspect_scout/_view/www/src/app/server/useTopicInvalidation.test.ts deleted file mode 100644 index 6c3553d4d..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useTopicInvalidation.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -// @vitest-environment jsdom -import { useQuery } from "@tanstack/react-query"; -import { renderHook, waitFor } from "@testing-library/react"; -import { sse } from "msw"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import { server } from "../../test/setup-msw"; -import { createTestWrapper } from "../../test/test-utils"; - -import { useTopicInvalidation } from "./useTopicInvalidation"; - -/** Subset of MSW's SSE client used in tests. */ -interface TestSSEClient { - send(payload: { data?: string }): void; - close(): void; -} - -/** - * Installs an SSE handler that captures the client reference for test control. - * Returns a promise that resolves with the client once the hook connects. - */ -function installSSEHandler(): Promise { - let resolveClient: (client: TestSSEClient) => void; - const clientReady = new Promise((r) => (resolveClient = r)); - - server.use( - sse("/api/v2/topics/stream", ({ client }) => { - resolveClient(client); - }) - ); - - return clientReady; -} - -describe("useTopicInvalidation", () => { - // The production code uses setTimeout(connect, 5000) for SSE reconnection. - // When EventSource.close() is called during cleanup, it can fire onerror - // synchronously, which schedules a new reconnect timer AFTER cleanup returns. - // Fake timers prevent this leaked timer from firing in subsequent tests. - // shouldAdvanceTime: true keeps waitFor working (it polls via setTimeout). - // The production code uses setTimeout(connect, 5000) for SSE reconnection. - // When EventSource.close() is called during cleanup, it can fire onerror - // synchronously, which schedules a new reconnect timer AFTER cleanup returns. - // Fake timers prevent this leaked timer from firing in subsequent tests. - // shouldAdvanceTime: true keeps waitFor working (it polls via setTimeout). - beforeEach(() => { - vi.useFakeTimers({ shouldAdvanceTime: true }); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it("returns false before connection, true after first message", async () => { - const clientReady = installSSEHandler(); - - const { result } = renderHook(() => useTopicInvalidation(), { - wrapper: createTestWrapper(), - }); - - expect(result.current).toBe(false); - - const client = await clientReady; - client.send({ data: JSON.stringify({ scans: "t1" }) }); - - await waitFor(() => { - expect(result.current).toBe(true); - }); - }); - - it("invalidates queries with matching topic key", async () => { - const clientReady = installSSEHandler(); - const wrapper = createTestWrapper(); - - // Track queryFn calls. Invalidation triggers a refetch, so the call count - // increasing proves invalidation happened. - const queryFn = vi.fn().mockResolvedValue({ items: [] }); - - const { result } = renderHook( - () => { - const ready = useTopicInvalidation(); - const q = useQuery({ - queryKey: ["scans", "/some/dir", "scans-inv"], - queryFn, - }); - return { ready, q }; - }, - { wrapper } - ); - - const client = await clientReady; - await waitFor(() => { - expect(result.current.q.isSuccess).toBe(true); - }); - - // First message establishes baseline — triggers invalidation for all topics - // (undefined → "t1") which causes a refetch. - client.send({ data: JSON.stringify({ scans: "t1" }) }); - await waitFor(() => { - expect(result.current.ready).toBe(true); - }); - await waitFor(() => { - expect(queryFn.mock.calls.length).toBeGreaterThanOrEqual(2); - }); - - const countAfterBaseline = queryFn.mock.calls.length; - - // Send a changed timestamp — should invalidate and trigger another refetch - client.send({ data: JSON.stringify({ scans: "t2" }) }); - - await waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(countAfterBaseline + 1); - }); - }); - - it("does not invalidate queries for unchanged topics", async () => { - const clientReady = installSSEHandler(); - const wrapper = createTestWrapper(); - - const queryFn = vi.fn().mockResolvedValue({ items: [] }); - - const { result } = renderHook( - () => { - const ready = useTopicInvalidation(); - const q = useQuery({ - queryKey: ["scans", "/dir", "scans-inv"], - queryFn, - }); - return { ready, q }; - }, - { wrapper } - ); - - const client = await clientReady; - await waitFor(() => { - expect(result.current.q.isSuccess).toBe(true); - }); - - // First message establishes baseline - client.send({ - data: JSON.stringify({ scans: "t1", "project-config": "p1" }), - }); - await waitFor(() => { - expect(result.current.ready).toBe(true); - }); - await waitFor(() => { - expect(queryFn.mock.calls.length).toBeGreaterThanOrEqual(2); - }); - - const countAfterBaseline = queryFn.mock.calls.length; - - // Send the same timestamps again — nothing should change - client.send({ - data: JSON.stringify({ scans: "t1", "project-config": "p1" }), - }); - - // Advance past any potential async processing - await vi.advanceTimersByTimeAsync(200); - expect(queryFn).toHaveBeenCalledTimes(countAfterBaseline); - }); - - it("closes EventSource on unmount", async () => { - const clientReady = installSSEHandler(); - - const { result, unmount } = renderHook(() => useTopicInvalidation(), { - wrapper: createTestWrapper(), - }); - - const client = await clientReady; - client.send({ data: JSON.stringify({ scans: "t1" }) }); - await waitFor(() => { - expect(result.current).toBe(true); - }); - - unmount(); - - // The 5s reconnect timer may have been scheduled by onerror during close. - // Advance past it to verify it doesn't cause errors (MSW handler was - // already consumed, so a reconnect would hit onUnhandledRequest: "error" - // if the timer leaked with real timers). - await vi.advanceTimersByTimeAsync(6000); - - expect(() => client.close()).not.toThrow(); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/server/useTopicInvalidation.ts b/src/inspect_scout/_view/www/src/app/server/useTopicInvalidation.ts deleted file mode 100644 index 68b64f8b0..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useTopicInvalidation.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useQueryClient } from "@tanstack/react-query"; -import { useEffect, useState, useRef } from "react"; - -import { TopicVersions } from "../../api/api"; -import { useApi } from "../../state/store"; - -/** - * Monitors topic updates via SSE and invalidates dependent queries on change. - * - * For each topic whose timestamp changes, invalidates all queries containing - * that topic name in their query key. - * - * Call once at app root level. - * - * @returns true when first SSE message received (ready), false otherwise - */ -export const useTopicInvalidation = (): boolean => { - const queryClient = useQueryClient(); - const versions = useTopicUpdates(); - const prevVersionsRef = useRef(undefined); - - useEffect(() => { - if (versions === undefined) return; - - const changedTopics = Object.entries(versions).filter( - ([topic, timestamp]) => prevVersionsRef.current?.[topic] !== timestamp - ); - for (const [topic] of changedTopics) { - const invKey = `${topic}-inv`; - void queryClient.invalidateQueries({ - predicate: (query) => query.queryKey.includes(invKey), - }); - } - - prevVersionsRef.current = versions; - }, [versions, queryClient]); - - return versions !== undefined; -}; - -/** - * Subscribes to SSE topic updates stream. - * Returns current topic versions dict, auto-reconnects on disconnect. - */ -const useTopicUpdates = (): TopicVersions | undefined => { - const api = useApi(); - const [versions, setVersions] = useState( - undefined - ); - - useEffect(() => api.connectTopicUpdates(setVersions), [api, setVersions]); - - return versions; -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useTranscript.test.ts b/src/inspect_scout/_view/www/src/app/server/useTranscript.test.ts deleted file mode 100644 index ce2864846..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useTranscript.test.ts +++ /dev/null @@ -1,357 +0,0 @@ -// @vitest-environment jsdom -import { skipToken } from "@tanstack/react-query"; -import { renderHook, waitFor } from "@testing-library/react"; -import { http, HttpResponse, passthrough } from "msw"; -import { beforeAll, expect, it } from "vitest"; -import { ZstdCodec } from "zstd-codec"; - -import { server } from "../../test/setup-msw"; -import { createTestWrapper } from "../../test/test-utils"; -import { MessagesEventsResponse, TranscriptInfo } from "../../types/api-types"; -import { encodeBase64Url } from "../../utils/base64url"; - -import { useTranscript } from "./useTranscript"; - -// Initialize zstd compression for tests -let compressZstd: (data: Uint8Array) => Uint8Array; - -beforeAll(async () => { - // zstd-codec loads WASM via a data URL that MSW intercepts. Passthrough - // prevents MSW from erroring on this unhandled request. - server.use(http.get(/octet-stream;base64,/, () => passthrough())); - - await new Promise((resolve) => { - ZstdCodec.run((zstd) => { - const simple = new zstd.Simple(); - compressZstd = (data: Uint8Array) => simple.compress(data); - resolve(); - }); - }); -}); - -const location = "/transcripts"; -const encodedLocation = encodeBase64Url(location); -const transcriptId = "sample-transcript-id"; -const encodedId = encodeURIComponent(transcriptId); - -const mockTranscriptInfo: TranscriptInfo = { - metadata: { task: "test-task" }, - model: "gpt-4", - date: "2025-01-15T10:00:00Z", - message_count: 5, - source_id: "test-source", - source_type: "eval", - source_uri: "/path/to/source.eval", - task_id: "test-task", - transcript_id: "test-uuid-123", -}; - -const mockMessagesEvents: MessagesEventsResponse = { - messages: [ - { - role: "user", - content: "Hello", - id: null, - metadata: null, - source: null, - tool_call_id: null, - }, - { - role: "assistant", - content: "Hi there!", - id: null, - metadata: null, - model: null, - source: null, - tool_calls: null, - }, - ], - events: [ - { - event: "info", - timestamp: "2025-01-15T10:00:00Z", - data: { sample: true }, - metadata: null, - pending: null, - source: null, - span_id: null, - uuid: null, - working_start: 0, - }, - ], -}; - -function assertZstdAcceptHeader(request: Request): Response | null { - const acceptEncoding = request.headers.get("X-Accept-Raw-Encoding"); - if (acceptEncoding !== "zstd") { - return HttpResponse.json( - { error: "Expected X-Accept-Raw-Encoding: zstd header" }, - { status: 400 } - ); - } - return null; -} - -function setupInfoHandler(): void { - server.use( - http.get(`/api/v2/transcripts/${encodedLocation}/${encodedId}/info`, () => - HttpResponse.json(mockTranscriptInfo) - ) - ); -} - -function setupMessagesEventsJsonHandler(): void { - server.use( - http.get( - `/api/v2/transcripts/${encodedLocation}/${encodedId}/messages-events`, - ({ request }) => { - const error = assertZstdAcceptHeader(request); - if (error) return error; - return HttpResponse.json(mockMessagesEvents); - } - ) - ); -} - -function setupMessagesEventsZstdHandler(): void { - server.use( - http.get( - `/api/v2/transcripts/${encodedLocation}/${encodedId}/messages-events`, - ({ request }) => { - const error = assertZstdAcceptHeader(request); - if (error) return error; - - const jsonString = JSON.stringify(mockMessagesEvents); - const encoder = new TextEncoder(); - const uncompressed = encoder.encode(jsonString); - const compressed = compressZstd(uncompressed); - - return new HttpResponse(compressed, { - status: 200, - headers: { - "Content-Type": "application/octet-stream", - "X-Content-Encoding": "zstd", - }, - }); - } - ) - ); -} - -it("returns loading then data on successful JSON fetch", async () => { - setupInfoHandler(); - setupMessagesEventsJsonHandler(); - - const { result } = renderHook( - () => useTranscript({ location, id: transcriptId }), - { wrapper: createTestWrapper() } - ); - - expect(result.current.loading).toBe(true); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeUndefined(); - expect(result.current.data).toBeDefined(); - expect(result.current.data?.model).toBe("gpt-4"); - expect(result.current.data?.messages).toHaveLength(2); - expect(result.current.data?.events).toHaveLength(1); -}); - -it("returns loading then data on successful zstd fetch", async () => { - setupInfoHandler(); - setupMessagesEventsZstdHandler(); - - const { result } = renderHook( - () => useTranscript({ location, id: transcriptId }), - { wrapper: createTestWrapper() } - ); - - expect(result.current.loading).toBe(true); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeUndefined(); - expect(result.current.data).toBeDefined(); - expect(result.current.data?.model).toBe("gpt-4"); - expect(result.current.data?.messages).toHaveLength(2); - expect(result.current.data?.events).toHaveLength(1); -}); - -it("does not make request when skipToken is passed", async () => { - let infoRequestMade = false; - let messagesEventsRequestMade = false; - - server.use( - http.get(`/api/v2/transcripts/${encodedLocation}/${encodedId}/info`, () => { - infoRequestMade = true; - return HttpResponse.json(mockTranscriptInfo); - }), - http.get( - `/api/v2/transcripts/${encodedLocation}/${encodedId}/messages-events`, - () => { - messagesEventsRequestMade = true; - return HttpResponse.json(mockMessagesEvents); - } - ) - ); - - const { result } = renderHook(() => useTranscript(skipToken), { - wrapper: createTestWrapper(), - }); - - expect(result.current.loading).toBe(true); - - await new Promise((r) => setTimeout(r, 50)); - expect(infoRequestMade).toBe(false); - expect(messagesEventsRequestMade).toBe(false); -}); - -it("returns error when info endpoint fails", async () => { - server.use( - http.get(`/api/v2/transcripts/${encodedLocation}/${encodedId}/info`, () => - HttpResponse.text("Server Error", { status: 500 }) - ), - http.get( - `/api/v2/transcripts/${encodedLocation}/${encodedId}/messages-events`, - () => HttpResponse.json(mockMessagesEvents) - ) - ); - - const { result } = renderHook( - () => useTranscript({ location, id: transcriptId }), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeDefined(); - expect(result.current.error?.message).toContain("500"); -}); - -it("returns error when messages-events endpoint fails", async () => { - setupInfoHandler(); - server.use( - http.get( - `/api/v2/transcripts/${encodedLocation}/${encodedId}/messages-events`, - () => HttpResponse.text("Server Error", { status: 500 }) - ) - ); - - const { result } = renderHook( - () => useTranscript({ location, id: transcriptId }), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeDefined(); - expect(result.current.error?.message).toContain("500"); -}); - -it("returns error for unsupported X-Content-Encoding", async () => { - setupInfoHandler(); - server.use( - http.get( - `/api/v2/transcripts/${encodedLocation}/${encodedId}/messages-events`, - () => { - return new HttpResponse("some data", { - status: 200, - headers: { - "Content-Type": "application/octet-stream", - "X-Content-Encoding": "unsupported-encoding", - }, - }); - } - ) - ); - - const { result } = renderHook( - () => useTranscript({ location, id: transcriptId }), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeDefined(); - expect(result.current.error?.message).toContain( - "Unsupported X-Content-Encoding" - ); -}); - -it("handles attachments in messages-events response", async () => { - const messagesWithAttachmentRefs: MessagesEventsResponse = { - messages: [ - { - role: "user", - content: "See attachment: {{att:image1}}", - id: null, - metadata: null, - source: null, - tool_call_id: null, - }, - ], - events: [], - attachments: { - image1: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYA", - }, - }; - - setupInfoHandler(); - server.use( - http.get( - `/api/v2/transcripts/${encodedLocation}/${encodedId}/messages-events`, - () => - HttpResponse.json(messagesWithAttachmentRefs) - ) - ); - - const { result } = renderHook( - () => useTranscript({ location, id: transcriptId }), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeUndefined(); - expect(result.current.data).toBeDefined(); - expect(result.current.data?.messages).toHaveLength(1); -}); - -it("sends correct X-Accept-Raw-Encoding header", async () => { - let capturedHeaders: Headers | undefined; - - setupInfoHandler(); - server.use( - http.get( - `/api/v2/transcripts/${encodedLocation}/${encodedId}/messages-events`, - ({ request }) => { - capturedHeaders = new Headers(request.headers); - return HttpResponse.json(mockMessagesEvents); - } - ) - ); - - const { result } = renderHook( - () => useTranscript({ location, id: transcriptId }), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(capturedHeaders?.get("X-Accept-Raw-Encoding")).toBe("zstd"); -}); diff --git a/src/inspect_scout/_view/www/src/app/server/useTranscript.ts b/src/inspect_scout/_view/www/src/app/server/useTranscript.ts deleted file mode 100644 index da3201227..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useTranscript.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; - -import { useApi } from "../../state/store"; -import { Transcript } from "../../types/api-types"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -type TranscriptParams = { - location: string; - id: string; -}; - -export const useTranscript = ( - params: TranscriptParams | typeof skipToken -): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: params === skipToken ? [skipToken] : ["transcript", params], - queryFn: - params === skipToken - ? skipToken - : () => api.getTranscript(params.location, params.id), - staleTime: Infinity, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useTranscriptsColumnValues.test.ts b/src/inspect_scout/_view/www/src/app/server/useTranscriptsColumnValues.test.ts deleted file mode 100644 index 8a5ffd347..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useTranscriptsColumnValues.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -// @vitest-environment jsdom -import { skipToken } from "@tanstack/react-query"; -import { renderHook, waitFor } from "@testing-library/react"; -import { http, HttpResponse } from "msw"; -import { describe, expect, it } from "vitest"; - -import { server } from "../../test/setup-msw"; -import { createTestWrapper } from "../../test/test-utils"; -import { encodeBase64Url } from "../../utils/base64url"; - -import { useTranscriptsColumnValues } from "./useTranscriptsColumnValues"; - -const location = "/transcripts"; -const encodedLocation = encodeBase64Url(location); - -describe("useTranscriptsColumnValues", () => { - it("returns loading then data on successful fetch", async () => { - const mockValues = ["gpt-4", "claude-3", "gemini"]; - - server.use( - http.post(`/api/v2/transcripts/${encodedLocation}/distinct`, () => - HttpResponse.json(mockValues) - ) - ); - - const { result } = renderHook( - () => - useTranscriptsColumnValues({ - location, - column: "model", - filter: undefined, - }), - { wrapper: createTestWrapper() } - ); - - expect(result.current.loading).toBe(true); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toEqual(mockValues); - }); - - it("sends column and filter in request body", async () => { - let capturedBody: unknown; - - server.use( - http.post( - `/api/v2/transcripts/${encodedLocation}/distinct`, - async ({ request }) => { - capturedBody = await request.json(); - return HttpResponse.json([]); - } - ) - ); - - const { result } = renderHook( - () => - useTranscriptsColumnValues({ - location, - column: "model", - filter: undefined, - }), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(capturedBody).toEqual({ column: "model", filter: null }); - }); - - it("returns error on server failure", async () => { - server.use( - http.post(`/api/v2/transcripts/${encodedLocation}/distinct`, () => - HttpResponse.text("Server Error", { status: 500 }) - ) - ); - - const { result } = renderHook( - () => - useTranscriptsColumnValues({ - location, - column: "model", - filter: undefined, - }), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeDefined(); - expect(result.current.error?.message).toContain("500"); - }); - - it("does not make request when skipToken is passed", async () => { - let requestMade = false; - - server.use( - http.post(`/api/v2/transcripts/${encodedLocation}/distinct`, () => { - requestMade = true; - return HttpResponse.json([]); - }) - ); - - const { result } = renderHook(() => useTranscriptsColumnValues(skipToken), { - wrapper: createTestWrapper(), - }); - - expect(result.current.loading).toBe(true); - - await new Promise((r) => setTimeout(r, 50)); - expect(requestMade).toBe(false); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/server/useTranscriptsColumnValues.ts b/src/inspect_scout/_view/www/src/app/server/useTranscriptsColumnValues.ts deleted file mode 100644 index 5ef32d6b5..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useTranscriptsColumnValues.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; - -import { ScalarValue } from "../../api/api"; -import { Condition } from "../../query"; -import { useApi } from "../../state/store"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -type TranscriptsColumnValuesParams = { - location: string; - column: string; - filter: Condition | undefined; -}; - -export const useTranscriptsColumnValues = ( - params: TranscriptsColumnValuesParams | typeof skipToken -): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: - params === skipToken ? [skipToken] : ["transcriptsColumnValues", params], - queryFn: - params === skipToken - ? skipToken - : () => - api.getTranscriptsColumnValues( - params.location, - params.column, - params.filter - ), - staleTime: 10 * 60 * 1000, // We can be pretty liberal here - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/server/useValidations.test.ts b/src/inspect_scout/_view/www/src/app/server/useValidations.test.ts deleted file mode 100644 index 6cb84945b..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useValidations.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -// @vitest-environment jsdom -import { skipToken } from "@tanstack/react-query"; -import { renderHook, waitFor, act } from "@testing-library/react"; -import { http, HttpResponse } from "msw"; -import { describe, expect, it } from "vitest"; - -import { server } from "../../test/setup-msw"; -import { createTestWrapper } from "../../test/test-utils"; -import type { ValidationCase } from "../../types/api-types"; -import { encodeBase64Url } from "../../utils/base64url"; - -import { - useBulkDeleteValidationCases, - useUpdateValidationCase, - useValidationCase, -} from "./useValidations"; - -const uri = "file:///validations/test.json"; -const caseId = "case-1"; -const encodedUri = encodeBase64Url(uri); -const encodedCaseId = encodeBase64Url(caseId); - -const mockCase: ValidationCase = { - id: caseId, - labels: { correct: true }, - target: "expected answer", - predicate: null, - split: null, -}; - -describe("useValidationCase", () => { - it("returns validation case on success", async () => { - server.use( - http.get(`/api/v2/validations/${encodedUri}/${encodedCaseId}`, () => - HttpResponse.json(mockCase) - ) - ); - - const { result } = renderHook( - () => useValidationCase({ url: uri, caseId }), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toEqual(mockCase); - }); - - it("returns null instead of error on 404", async () => { - server.use( - http.get(`/api/v2/validations/${encodedUri}/${encodedCaseId}`, () => - HttpResponse.text("Not Found", { status: 404 }) - ) - ); - - const { result } = renderHook( - () => useValidationCase({ url: uri, caseId }), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.data).toBeNull(); - expect(result.current.error).toBeUndefined(); - }); - - it("returns error on non-404 failures", async () => { - server.use( - http.get(`/api/v2/validations/${encodedUri}/${encodedCaseId}`, () => - HttpResponse.text("Server Error", { status: 500 }) - ) - ); - - const { result } = renderHook( - () => useValidationCase({ url: uri, caseId }), - { wrapper: createTestWrapper() } - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - expect(result.current.error).toBeDefined(); - expect(result.current.error?.message).toContain("500"); - }); - - it("does not make request when skipToken is passed", async () => { - let requestMade = false; - - server.use( - http.get(`/api/v2/validations/${encodedUri}/${encodedCaseId}`, () => { - requestMade = true; - return HttpResponse.json(mockCase); - }) - ); - - const { result } = renderHook(() => useValidationCase(skipToken), { - wrapper: createTestWrapper(), - }); - - expect(result.current.loading).toBe(true); - - await new Promise((r) => setTimeout(r, 50)); - expect(requestMade).toBe(false); - }); -}); - -describe("useUpdateValidationCase", () => { - it("sends update and returns updated case", async () => { - let capturedBody: unknown; - - const updatedCase = { ...mockCase, labels: { correct: false } }; - - server.use( - http.post( - `/api/v2/validations/${encodedUri}/${encodedCaseId}`, - async ({ request }) => { - capturedBody = await request.json(); - return HttpResponse.json(updatedCase); - } - ) - ); - - const { result } = renderHook(() => useUpdateValidationCase(uri), { - wrapper: createTestWrapper(), - }); - - const updateData = { labels: { correct: false } }; - - await act(() => result.current.mutateAsync({ caseId, data: updateData })); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(capturedBody).toEqual(updateData); - expect(result.current.data).toEqual(updatedCase); - }); - - it("rolls back optimistic update on error", async () => { - const wrapper = createTestWrapper(); - - // Pre-populate the case cache - server.use( - http.get(`/api/v2/validations/${encodedUri}/${encodedCaseId}`, () => - HttpResponse.json(mockCase) - ) - ); - - const { result: caseResult } = renderHook( - () => useValidationCase({ url: uri, caseId }), - { wrapper } - ); - - await waitFor(() => { - expect(caseResult.current.loading).toBe(false); - }); - - expect(caseResult.current.data).toEqual(mockCase); - - // Now set up the update to fail - server.use( - http.post(`/api/v2/validations/${encodedUri}/${encodedCaseId}`, () => - HttpResponse.text("Server Error", { status: 500 }) - ) - ); - - const { result: mutationResult } = renderHook( - () => useUpdateValidationCase(uri), - { wrapper } - ); - - await act(async () => { - try { - await mutationResult.current.mutateAsync({ - caseId, - data: { labels: { correct: false } }, - }); - } catch { - // expected - } - }); - - await waitFor(() => { - expect(mutationResult.current.isError).toBe(true); - }); - - // After rollback, the cache should have the original data - await waitFor(() => { - expect(caseResult.current.data).toEqual(mockCase); - }); - }); -}); - -describe("useBulkDeleteValidationCases", () => { - const caseIds = ["case-1", "case-2", "case-3"]; - - const deleteEndpoint = (id: string) => - `/api/v2/validations/${encodedUri}/${encodeBase64Url(id)}`; - - it("deletes all cases successfully", async () => { - for (const id of caseIds) { - server.use( - http.delete( - deleteEndpoint(id), - () => new HttpResponse(null, { status: 204 }) - ) - ); - } - - const { result } = renderHook(() => useBulkDeleteValidationCases(uri), { - wrapper: createTestWrapper(), - }); - - await act(() => result.current.mutateAsync(caseIds)); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.data).toEqual({ succeeded: 3, failed: 0 }); - }); - - it("handles partial failures gracefully", async () => { - // case-1 and case-3 succeed, case-2 fails - server.use( - http.delete( - deleteEndpoint("case-1"), - () => new HttpResponse(null, { status: 204 }) - ), - http.delete(deleteEndpoint("case-2"), () => - HttpResponse.text("Server Error", { status: 500 }) - ), - http.delete( - deleteEndpoint("case-3"), - () => new HttpResponse(null, { status: 204 }) - ) - ); - - const { result } = renderHook(() => useBulkDeleteValidationCases(uri), { - wrapper: createTestWrapper(), - }); - - await act(() => result.current.mutateAsync(caseIds)); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.data).toEqual({ succeeded: 2, failed: 1 }); - }); - - it("throws when all deletions fail", async () => { - for (const id of caseIds) { - server.use( - http.delete(deleteEndpoint(id), () => - HttpResponse.text("Server Error", { status: 500 }) - ) - ); - } - - const { result } = renderHook(() => useBulkDeleteValidationCases(uri), { - wrapper: createTestWrapper(), - }); - - await act(async () => { - try { - await result.current.mutateAsync(caseIds); - } catch { - // expected - } - }); - - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); - - expect(result.current.error?.message).toContain("All deletions failed"); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/server/useValidations.ts b/src/inspect_scout/_view/www/src/app/server/useValidations.ts deleted file mode 100644 index a87bca626..000000000 --- a/src/inspect_scout/_view/www/src/app/server/useValidations.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { skipToken, useMutation, useQueryClient } from "@tanstack/react-query"; - -import { ApiError } from "../../api/request"; -import { useApi } from "../../state/store"; -import { - CreateValidationSetRequest, - ValidationCase, - ValidationCaseRequest, -} from "../../types/api-types"; -import { AsyncData } from "../../utils/asyncData"; -import { useAsyncDataFromQuery } from "../../utils/asyncDataFromQuery"; - -/** - * Query key factory for validation-related queries. - * Centralizes key definitions to ensure consistency between queries and invalidations. - */ -export const validationQueryKeys = { - sets: () => ["validationSets"] as const, - cases: (uri: string | typeof skipToken) => ["validationCases", uri] as const, - case: (params: { url: string; caseId: string } | typeof skipToken) => - ["validationCase", params] as const, -}; - -/** - * Hook to fetch all validation set URIs in the project. - */ -export const useValidationSets = (): AsyncData => { - const api = useApi(); - return useAsyncDataFromQuery({ - queryKey: validationQueryKeys.sets(), - queryFn: () => api.getValidationSets(), - staleTime: 60 * 1000, - }); -}; - -/** - * Hook to fetch validation cases for a specific validation set. - */ -export const useValidationCases = ( - uri: string | typeof skipToken -): AsyncData => { - const api = useApi(); - return useAsyncDataFromQuery({ - queryKey: validationQueryKeys.cases(uri), - queryFn: uri === skipToken ? skipToken : () => api.getValidationCases(uri), - staleTime: 60 * 1000, - enabled: uri !== skipToken, - }); -}; - -/** - * Hook to fetch a single validation case by URI and case ID. - * Returns null (not an error) when the case is not found (404). - */ -export const useValidationCase = ( - params: { url: string; caseId: string } | typeof skipToken -): AsyncData => { - const api = useApi(); - - return useAsyncDataFromQuery({ - queryKey: validationQueryKeys.case(params), - queryFn: - params === skipToken - ? skipToken - : async () => { - try { - return await api.getValidationCase(params.url, params.caseId); - } catch (error) { - if (error instanceof ApiError && error.status === 404) { - return null; - } - throw error; - } - }, - staleTime: 60 * 1000, - }); -}; - -/** - * Hook to create a new validation set. - */ -export const useCreateValidationSet = () => { - const queryClient = useQueryClient(); - const api = useApi(); - return useMutation({ - mutationFn: (request) => api.createValidationSet(request), - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: validationQueryKeys.sets(), - }); - }, - }); -}; - -/** - * Hook to update a validation case (upsert). - * Uses optimistic updates to prevent UI flicker during save. - */ -export const useUpdateValidationCase = (uri: string) => { - const queryClient = useQueryClient(); - const api = useApi(); - return useMutation< - ValidationCase, - Error, - { caseId: string; data: ValidationCaseRequest }, - { - previousCase: ValidationCase | undefined; - previousCases: ValidationCase[] | undefined; - } - >({ - mutationFn: ({ caseId, data }) => - api.upsertValidationCase(uri, caseId, data), - - onMutate: ({ caseId, data }) => { - // Cancel any outgoing refetches to avoid overwriting optimistic update - void queryClient.cancelQueries({ - queryKey: validationQueryKeys.case({ url: uri, caseId }), - }); - void queryClient.cancelQueries({ - queryKey: validationQueryKeys.cases(uri), - }); - - // Snapshot the previous values for rollback - const previousCase = queryClient.getQueryData( - validationQueryKeys.case({ url: uri, caseId }) - ); - const previousCases = queryClient.getQueryData( - validationQueryKeys.cases(uri) - ); - - // Optimistically update both caches - if (previousCase) { - queryClient.setQueryData( - validationQueryKeys.case({ url: uri, caseId }), - { ...previousCase, ...data } - ); - } - if (previousCases) { - queryClient.setQueryData( - validationQueryKeys.cases(uri), - previousCases.map((c) => (c.id === caseId ? { ...c, ...data } : c)) - ); - } - - return { previousCase, previousCases }; - }, - - onError: (_err, { caseId }, context) => { - // Rollback to previous values on error - if (context?.previousCase) { - queryClient.setQueryData( - validationQueryKeys.case({ url: uri, caseId }), - context.previousCase - ); - } - if (context?.previousCases) { - queryClient.setQueryData( - validationQueryKeys.cases(uri), - context.previousCases - ); - } - }, - - onSuccess: (_data, { caseId }) => { - // Invalidate both queries to sync with server. - // Since we've already optimistically updated both caches, the invalidation - // will refetch in the background without causing UI flicker. - void queryClient.invalidateQueries({ - queryKey: validationQueryKeys.case({ url: uri, caseId }), - }); - void queryClient.invalidateQueries({ - queryKey: validationQueryKeys.cases(uri), - }); - }, - }); -}; - -/** - * Hook to delete a single validation case. - */ -export const useDeleteValidationCase = (uri: string) => { - const queryClient = useQueryClient(); - const api = useApi(); - return useMutation({ - mutationFn: (caseId) => api.deleteValidationCase(uri, caseId), - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: validationQueryKeys.cases(uri), - }); - }, - }); -}; - -/** - * Hook to delete multiple validation cases (bulk delete). - * Uses Promise.allSettled to handle partial failures gracefully. - */ -export const useBulkDeleteValidationCases = (uri: string) => { - const queryClient = useQueryClient(); - const api = useApi(); - return useMutation<{ succeeded: number; failed: number }, Error, string[]>({ - mutationFn: async (caseIds) => { - const results = await Promise.allSettled( - caseIds.map((id) => api.deleteValidationCase(uri, id)) - ); - - const succeeded = results.filter((r) => r.status === "fulfilled").length; - const failed = results.filter((r) => r.status === "rejected").length; - - // Always invalidate cache if at least one succeeded - if (succeeded > 0) { - void queryClient.invalidateQueries({ - queryKey: validationQueryKeys.cases(uri), - }); - } - - // Throw if all failed - if (failed === results.length) { - const errors = results - .filter((r): r is PromiseRejectedResult => r.status === "rejected") - .map((r) => r.reason); - throw new Error(`All deletions failed: ${errors.join(", ")}`); - } - - return { succeeded, failed }; - }, - }); -}; - -/** - * Hook to delete an entire validation set. - */ -export const useDeleteValidationSet = () => { - const queryClient = useQueryClient(); - const api = useApi(); - return useMutation({ - mutationFn: (uri) => api.deleteValidationSet(uri), - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: validationQueryKeys.sets(), - }); - }, - }); -}; - -/** - * Hook to rename a validation set. - */ -export const useRenameValidationSet = () => { - const queryClient = useQueryClient(); - const api = useApi(); - return useMutation({ - mutationFn: ({ uri, newName }) => api.renameValidationSet(uri, newName), - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: validationQueryKeys.sets(), - }); - }, - }); -}; diff --git a/src/inspect_scout/_view/www/src/app/transcript/TranscriptBody.module.css b/src/inspect_scout/_view/www/src/app/transcript/TranscriptBody.module.css deleted file mode 100644 index 2111db854..000000000 --- a/src/inspect_scout/_view/www/src/app/transcript/TranscriptBody.module.css +++ /dev/null @@ -1,103 +0,0 @@ -.tabContainer { - display: grid; - grid-template-rows: max-content minmax(0, 1fr); - height: 100%; -} - -:global(.nav).tabs { - border-bottom: solid 1px var(--bs-border-color); - padding-bottom: 0.25rem; - padding-top: 0.5rem; - padding-left: 0.5rem; - padding-right: 0.5rem; - position: sticky; - z-index: 999; - top: 0; - background-color: var(--bs-body-bg); -} - -:global(.nav).tabs :global(.nav-link) { - padding: 0.3rem 0.5rem; - font-size: var(--inspect-font-size-smallest) !important; -} - -.chatTab > * > * { - padding: 0.5rem; -} - -.chatTab { - padding-bottom: 50px; -} - -.metadata { - padding: 0.5rem; - padding-bottom: 50px; -} - -.scrollable { - overflow-y: auto; - min-height: 0; - height: 100%; -} - -.eventsSeparator { - background-color: var(--bs-border-color); -} - -.eventsList { - padding-bottom: 1rem; -} - -.eventsContainer { - display: grid; - width: 100%; - grid-template-columns: 180px 1px 1fr; - min-height: 100vh; - transition: grid-template-columns 0.3s ease; - padding-bottom: 44px; -} - -.eventsContainer.outlineCollapsed { - grid-template-columns: 28px 1px 1fr; -} - -.eventsTab { - display: flex; -} - -.eventsOutline { - padding-left: 0.5rem; - padding-top: 0.8rem; -} - -.outlineToggle { - cursor: pointer; - font-size: 0.8em; - position: absolute; - top: 0.5em; - right: 0.5em; -} - -.tabTool { - font-size: var(--inspect-font-size-smallestest) !important; - padding: 0.1rem 0.4rem !important; -} - -.splitLayout { - height: 100%; - width: 100%; -} - -.splitStart { - height: 100%; - overflow-y: auto; -} - -.validationSidebar { - display: flex; - flex-direction: column; - height: 100%; - border-left: 1px solid var(--bs-border-color); - background-color: var(--bs-body-bg); - overflow-y: auto; -} diff --git a/src/inspect_scout/_view/www/src/app/transcript/TranscriptBody.tsx b/src/inspect_scout/_view/www/src/app/transcript/TranscriptBody.tsx deleted file mode 100644 index 026205fc1..000000000 --- a/src/inspect_scout/_view/www/src/app/transcript/TranscriptBody.tsx +++ /dev/null @@ -1,510 +0,0 @@ -import { VscodeSplitLayout } from "@vscode-elements/react-elements"; -import clsx from "clsx"; -import { - FC, - ReactNode, - RefObject, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { useSearchParams } from "react-router-dom"; - -import { ChatViewVirtualList } from "../../components/chat/ChatViewVirtualList"; -import { DisplayModeContext } from "../../components/content/DisplayModeContext"; -import { MetaDataGrid } from "../../components/content/MetaDataGrid"; -import { ApplicationIcons } from "../../components/icons"; -import { StickyScroll } from "../../components/StickyScroll"; -import { TabPanel, TabSet } from "../../components/TabSet"; -import { ToolButton } from "../../components/ToolButton"; -import { ToolDropdownButton } from "../../components/ToolDropdownButton"; -import { useEventNodes } from "../../components/transcript/hooks/useEventNodes"; -import { TranscriptOutline } from "../../components/transcript/outline/TranscriptOutline"; -import { TranscriptViewNodes } from "../../components/transcript/TranscriptViewNodes"; -import { - EventNode, - kCollapsibleEventTypes, - kTranscriptCollapseScope, -} from "../../components/transcript/types"; -import { getValidationParam, updateValidationParam } from "../../router/url"; -import { useStore } from "../../state/store"; -import { Transcript } from "../../types/api-types"; -import { messagesToStr } from "../utils/messages"; -import { ValidationCaseEditor } from "../validation/components/ValidationCaseEditor"; - -import { useTranscriptColumnFilter } from "./hooks/useTranscriptColumnFilter"; -import styles from "./TranscriptBody.module.css"; -import { TranscriptFilterPopover } from "./TranscriptFilterPopover"; - -export const kTranscriptMessagesTabId = "transcript-messages"; -export const kTranscriptEventsTabId = "transcript-events"; -export const kTranscriptMetadataTabId = "transcript-metadata"; -export const kTranscriptInfoTabId = "transcript-info"; - -/** - * Recursively collects all collapsible event IDs from the event tree. - */ -const collectAllCollapsibleIds = ( - nodes: EventNode[] -): Record => { - const result: Record = {}; - const traverse = (nodeList: EventNode[]) => { - for (const node of nodeList) { - if (kCollapsibleEventTypes.includes(node.event.event)) { - result[node.id] = true; - } - if (node.children.length > 0) { - traverse(node.children); - } - } - }; - traverse(nodes); - return result; -}; - -interface TranscriptBodyProps { - transcript: Transcript; - scrollRef: RefObject; -} - -export const TranscriptBody: FC = ({ - transcript, - scrollRef, -}) => { - const [searchParams, setSearchParams] = useSearchParams(); - const tabParam = searchParams.get("tab"); - - // Get event or message ID from query params for deep linking - const eventParam = searchParams.get("event"); - const messageParam = searchParams.get("message"); - - // Selected tab - const selectedTranscriptTab = useStore( - (state) => state.selectedTranscriptTab - ); - const setSelectedTranscriptTab = useStore( - (state) => state.setSelectedTranscriptTab - ); - const resolvedSelectedTranscriptTab = - tabParam || selectedTranscriptTab || kTranscriptMessagesTabId; - - const handleTabChange = useCallback( - (tabId: string) => { - // update both store and URL - setSelectedTranscriptTab(tabId); - setSearchParams((prevParams) => { - const newParams = new URLSearchParams(prevParams); - newParams.set("tab", tabId); - return newParams; - }); - }, - [setSelectedTranscriptTab, setSearchParams] - ); - - // Auto-switch tab based on deep link params - useEffect(() => { - if ( - eventParam && - resolvedSelectedTranscriptTab !== kTranscriptEventsTabId - ) { - handleTabChange(kTranscriptEventsTabId); - } else if ( - messageParam && - resolvedSelectedTranscriptTab !== kTranscriptMessagesTabId - ) { - handleTabChange(kTranscriptMessagesTabId); - } - }, [ - eventParam, - messageParam, - resolvedSelectedTranscriptTab, - handleTabChange, - ]); - - // Transcript Filtering - const transcriptFilterButtonRef = useRef(null); - const [transcriptFilterShowing, setTranscriptFilterShowing] = useState(false); - const toggleTranscriptFilterShowing = useCallback(() => { - setTranscriptFilterShowing((prev) => !prev); - }, []); - const { excludedEventTypes, isDebugFilter, isDefaultFilter } = - useTranscriptColumnFilter(); - - const filteredEvents = useMemo(() => { - if (excludedEventTypes.length === 0) { - return transcript.events; - } - return transcript.events.filter((event) => { - return !excludedEventTypes.includes(event.event); - }); - }, [transcript.events, excludedEventTypes]); - - // Transcript event data - const { eventNodes, defaultCollapsedIds } = useEventNodes( - filteredEvents, - false - ); - - // Transcript collapse - const eventsCollapsed = useStore((state) => state.transcriptState.collapsed); - const setTranscriptState = useStore((state) => state.setTranscriptState); - const collapseEvents = useCallback( - (collapsed: boolean) => { - setTranscriptState((prev) => ({ - ...prev, - collapsed, - })); - }, - [setTranscriptState] - ); - - const setCollapsedEvents = useStore( - (state) => state.setTranscriptCollapsedEvents - ); - - // Outline toggle - const outlineCollapsed = useStore( - (state) => state.transcriptState.outlineCollapsed - ); - const toggleOutline = useCallback( - (collapsed: boolean) => { - setTranscriptState((prev) => ({ - ...prev, - outlineCollapsed: collapsed, - })); - }, - [setTranscriptState] - ); - - // Validation sidebar - URL is the source of truth - const validationSidebarCollapsed = !getValidationParam(searchParams); - - const toggleValidationSidebar = useCallback(() => { - setSearchParams((prevParams) => { - const isCurrentlyOpen = getValidationParam(prevParams); - return updateValidationParam(prevParams, !isCurrentlyOpen); - }); - }, [setSearchParams]); - - // Display mode for raw/rendered text - const displayMode = useStore( - (state) => state.transcriptState.displayMode ?? "rendered" - ); - - const toggleDisplayMode = useCallback(() => { - setTranscriptState((prev) => ({ - ...prev, - displayMode: prev.displayMode === "raw" ? "rendered" : "raw", - })); - }, [setTranscriptState]); - - const displayModeContextValue = useMemo( - () => ({ displayMode }), - [displayMode] - ); - - useEffect(() => { - if (transcript.events.length <= 0 || eventsCollapsed === undefined) { - return; - } - - if (!eventsCollapsed && Object.keys(defaultCollapsedIds).length > 0) { - setCollapsedEvents(kTranscriptCollapseScope, defaultCollapsedIds); - } else if (eventsCollapsed) { - const allCollapsibleIds = collectAllCollapsibleIds(eventNodes); - setCollapsedEvents(kTranscriptCollapseScope, allCollapsibleIds); - } - }, [ - defaultCollapsedIds, - eventNodes, - eventsCollapsed, - setCollapsedEvents, - transcript.events.length, - ]); - - const tabTools: ReactNode[] = []; - - if (resolvedSelectedTranscriptTab === kTranscriptEventsTabId) { - const label = isDebugFilter - ? "Debug" - : isDefaultFilter - ? "Default" - : "Custom"; - - tabTools.push( - - ); - - tabTools.push( - { - collapseEvents(!eventsCollapsed); - }} - subtle={true} - /> - ); - } - - tabTools.push( - - ); - - tabTools.push( - - ); - - tabTools.push( - - ); - - const tabPanels = [ - { - handleTabChange(kTranscriptMessagesTabId); - }} - selected={resolvedSelectedTranscriptTab === kTranscriptMessagesTabId} - scrollable={false} - > - - , - ]; - - if (transcript.events && transcript.events.length > 0) { - tabPanels.push( - { - handleTabChange(kTranscriptEventsTabId); - }} - selected={resolvedSelectedTranscriptTab === kTranscriptEventsTabId} - scrollable={false} - > -
- - {!outlineCollapsed && ( - - )} -
toggleOutline(!outlineCollapsed)} - > - -
-
-
- -
- - - ); - } - - if (transcript.metadata && Object.keys(transcript.metadata).length > 0) { - tabPanels.push( - { - handleTabChange(kTranscriptMetadataTabId); - }} - selected={resolvedSelectedTranscriptTab === kTranscriptMetadataTabId} - scrollable={false} - > -
- -
-
- ); - } - - const { events, messages, metadata, ...infoData } = transcript; - tabPanels.push( - { - handleTabChange(kTranscriptInfoTabId); - }} - selected={resolvedSelectedTranscriptTab === kTranscriptInfoTabId} - scrollable={false} - > -
- -
-
- ); - - const tabSetContent = ( - - {tabPanels} - - ); - - return ( - - {validationSidebarCollapsed ? ( - tabSetContent - ) : ( - -
- {tabSetContent} -
-
- -
-
- )} -
- ); -}; - -const CopyToolbarButton: FC<{ - transcript: Transcript; - className?: string | string[]; -}> = ({ transcript, className }) => { - const [icon, setIcon] = useState(ApplicationIcons.copy); - - const showCopyConfirmation = useCallback(() => { - setIcon(ApplicationIcons.confirm); - setTimeout(() => setIcon(ApplicationIcons.copy), 1250); - }, []); - - if (!transcript) { - return undefined; - } - - return ( - { - if (transcript.transcript_id) { - void navigator.clipboard.writeText(transcript.transcript_id); - showCopyConfirmation(); - } - }, - Transcript: () => { - if (transcript.messages) { - void navigator.clipboard.writeText( - messagesToStr(transcript.messages) - ); - showCopyConfirmation(); - } - }, - }} - /> - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/transcript/TranscriptFilterPopover.module.css b/src/inspect_scout/_view/www/src/app/transcript/TranscriptFilterPopover.module.css deleted file mode 100644 index b060cd6cd..000000000 --- a/src/inspect_scout/_view/www/src/app/transcript/TranscriptFilterPopover.module.css +++ /dev/null @@ -1,42 +0,0 @@ -.grid { - display: grid; - grid-template-columns: 1fr 1fr; - column-gap: 2em; - row-gap: 0.15em; -} - -.row { - display: flex; - align-items: center; - gap: 0.5em; - cursor: pointer; - border-radius: var(--bs-border-radius); - padding: 0.1em 0.4em; - margin: 0 -0.4em; -} - -.row:hover { - background-color: var(--bs-secondary-bg); -} - -.links { - display: flex; - padding-bottom: 0.2em; - margin-bottom: 0.4em; - column-gap: 0.3em; - border-bottom: solid 1px var(--bs-border-color); -} - -.links a { - cursor: pointer; - text-decoration: none; - color: var(--bs-link-color); -} - -.links a:hover { - color: var(--bs-link-hover-color); -} - -.selected { - font-weight: 600; -} diff --git a/src/inspect_scout/_view/www/src/app/transcript/TranscriptFilterPopover.tsx b/src/inspect_scout/_view/www/src/app/transcript/TranscriptFilterPopover.tsx deleted file mode 100644 index 62486f631..000000000 --- a/src/inspect_scout/_view/www/src/app/transcript/TranscriptFilterPopover.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { PopOver } from "../../components/PopOver"; - -import { useTranscriptColumnFilter } from "./hooks/useTranscriptColumnFilter"; -import styles from "./TranscriptFilterPopover.module.css"; - -export interface TranscriptFilterProps { - showing: boolean; - setShowing: (showing: boolean) => void; - positionEl: HTMLElement | null; -} - -export const TranscriptFilterPopover: FC = ({ - showing, - positionEl, - setShowing, -}) => { - const { - excludedEventTypes, - isDefaultFilter, - isDebugFilter, - setDefaultFilter, - setDebugFilter, - toggleEventType, - arrangedEventTypes, - } = useTranscriptColumnFilter(); - - return ( - - - -
- {arrangedEventTypes(2).map((eventType) => { - const isExcluded = excludedEventTypes.includes(eventType); - return ( -
{ - toggleEventType(eventType, isExcluded); - }} - > - { - e.stopPropagation(); - toggleEventType(eventType, isExcluded); - }} - /> - {eventType} -
- ); - })} -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/transcript/TranscriptNav.tsx b/src/inspect_scout/_view/www/src/app/transcript/TranscriptNav.tsx deleted file mode 100644 index c99b29338..000000000 --- a/src/inspect_scout/_view/www/src/app/transcript/TranscriptNav.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { FC } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { useLoggingNavigate } from "../../debugging/navigationDebugging"; -import { transcriptRoute } from "../../router/url"; -import { Transcript } from "../../types/api-types"; -import { NextPreviousNav } from "../components/NextPreviousNav"; -import { TaskName } from "../components/TaskName"; - -interface TranscriptNavProps { - transcriptsDir: string; - transcript?: Transcript; - prevId?: string; - nextId?: string; -} - -export const TranscriptNav: FC = ({ - transcriptsDir, - transcript, - prevId, - nextId, -}) => { - const navigate = useLoggingNavigate("TranscriptNav"); - const [searchParams] = useSearchParams(); - - const handlePrevious = () => { - if (prevId) { - void navigate(transcriptRoute(transcriptsDir, prevId, searchParams)); - } - }; - - const handleNext = () => { - if (nextId) { - void navigate(transcriptRoute(transcriptsDir, nextId, searchParams)); - } - }; - - return ( - - {transcript && ( - - )} - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/transcript/TranscriptPanel.module.css b/src/inspect_scout/_view/www/src/app/transcript/TranscriptPanel.module.css deleted file mode 100644 index 773e7e383..000000000 --- a/src/inspect_scout/_view/www/src/app/transcript/TranscriptPanel.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.container { - display: grid; - grid-template-rows: max-content max-content 1fr; - height: 100vh; -} - -.transcriptContainer { - overflow-y: auto; - width: 100%; - min-height: 0; -} diff --git a/src/inspect_scout/_view/www/src/app/transcript/TranscriptPanel.tsx b/src/inspect_scout/_view/www/src/app/transcript/TranscriptPanel.tsx deleted file mode 100644 index b32d846c4..000000000 --- a/src/inspect_scout/_view/www/src/app/transcript/TranscriptPanel.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; -import clsx from "clsx"; -import { FC, useRef } from "react"; - -import { ApiError } from "../../api/request"; -import { ErrorPanel } from "../../components/ErrorPanel"; -import { LoadingBar } from "../../components/LoadingBar"; -import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import { useStore } from "../../state/store"; -import { useRequiredParams } from "../../utils/router"; -import { TranscriptsNavbar } from "../components/TranscriptsNavbar"; -import { useFilterConditions } from "../hooks/useFilterConditions"; -import { useAdjacentTranscriptIds } from "../server/useAdjacentTranscriptIds"; -import { useAppConfig } from "../server/useAppConfig"; -import { useTranscript } from "../server/useTranscript"; -import { TRANSCRIPTS_INFINITE_SCROLL_CONFIG } from "../transcripts/constants"; -import { getTranscriptDisplayName } from "../utils/transcript"; -import { useTranscriptsDir } from "../utils/useTranscriptsDir"; - -import { TranscriptBody } from "./TranscriptBody"; -import { TranscriptNav } from "./TranscriptNav"; -import styles from "./TranscriptPanel.module.css"; -import { TranscriptTitle } from "./TranscriptTitle"; - -export const TranscriptPanel: FC = () => { - // The core scroll element for the transcript panel - const scrollRef = useRef(null); - - // Transcript data from route - const { transcriptId } = useRequiredParams("transcriptId"); - - // Transcripts directory (resolved from route, user preference, or config) - const { - displayTranscriptsDir, - resolvedTranscriptsDir, - resolvedTranscriptsDirSource, - setTranscriptsDir, - } = useTranscriptsDir(true); - - // Server transcripts directory - const config = useAppConfig(); - const { - loading, - data: transcript, - error, - } = useTranscript( - config.transcripts - ? { location: config.transcripts.dir, id: transcriptId } - : skipToken - ); - const filter = Array.isArray(config.filter) - ? config.filter.join(" ") - : config.filter; - - // Set document title with transcript task name - useDocumentTitle(getTranscriptDisplayName(transcript), "Transcripts"); - - // Get sorting/filter from store - const sorting = useStore((state) => state.transcriptsTableState.sorting); - const condition = useFilterConditions(); - - // Get adjacent transcript IDs - const adjacentIds = useAdjacentTranscriptIds( - transcriptId, - resolvedTranscriptsDir, - TRANSCRIPTS_INFINITE_SCROLL_CONFIG.pageSize, - condition, - sorting - ); - const [prevId, nextId] = adjacentIds.data ?? [undefined, undefined]; - - return ( -
- - - - - - {!error && transcript && ( -
- - -
- )} - {error && ( - - )} -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/transcript/TranscriptTitle.module.css b/src/inspect_scout/_view/www/src/app/transcript/TranscriptTitle.module.css deleted file mode 100644 index e1141ce91..000000000 --- a/src/inspect_scout/_view/www/src/app/transcript/TranscriptTitle.module.css +++ /dev/null @@ -1,6 +0,0 @@ -.titleContainer { - margin-left: 0.5rem; - margin-right: 0.5rem; - margin-top: 0.5rem; - margin-bottom: 1rem; -} diff --git a/src/inspect_scout/_view/www/src/app/transcript/TranscriptTitle.tsx b/src/inspect_scout/_view/www/src/app/transcript/TranscriptTitle.tsx deleted file mode 100644 index 26dc2bbd2..000000000 --- a/src/inspect_scout/_view/www/src/app/transcript/TranscriptTitle.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import clsx from "clsx"; -import { FC } from "react"; - -import { Transcript } from "../../types/api-types"; -import { formatDateTime, formatNumber, formatTime } from "../../utils/format"; -import { HeadingGrid, HeadingValue } from "../components/HeadingGrid"; -import { ScoreValue } from "../components/ScoreValue"; -import { TaskName } from "../components/TaskName"; - -import styles from "./TranscriptTitle.module.css"; - -interface TranscriptTitleProps { - transcript: Transcript; -} - -export const TranscriptTitle: FC = ({ transcript }) => { - const cols: HeadingValue[] = [ - { - label: "Transcript", - value: ( - - ), - }, - ]; - - if (transcript.date) { - cols.push({ - label: "Date", - value: formatDateTime(new Date(transcript.date)), - }); - } - - if (transcript.agent) { - cols.push({ - label: "Agent", - value: transcript.agent, - }); - } - - if (transcript.model) { - cols.push({ - label: "Model", - value: transcript.model, - }); - } - - if (transcript.limit) { - cols.push({ - label: "Limit", - value: transcript.limit, - }); - } - - if (transcript.error) { - cols.push({ - label: "Error", - value: transcript.error, - }); - } - - if (transcript.total_tokens) { - cols.push({ - label: "Tokens", - value: formatNumber(transcript.total_tokens), - }); - } - - if (transcript.total_time) { - cols.push({ - label: "Time", - value: formatTime(transcript.total_time), - }); - } - - if (transcript.message_count) { - cols.push({ - label: "Messages", - value: transcript.message_count.toString(), - }); - } - - if (transcript.score) { - cols.push({ - label: "Score", - value: , - }); - } - - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/transcript/hooks/useTranscriptColumnFilter.ts b/src/inspect_scout/_view/www/src/app/transcript/hooks/useTranscriptColumnFilter.ts deleted file mode 100644 index 7a3eaf897..000000000 --- a/src/inspect_scout/_view/www/src/app/transcript/hooks/useTranscriptColumnFilter.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { useCallback, useMemo } from "react"; - -import { - eventTypeValues, - EventTypeValue, -} from "../../../components/transcript/types"; -import { useStore } from "../../../state/store"; - -export const kDefaultExcludedEventTypes: EventTypeValue[] = [ - "sample_init", - "sandbox", - "state", - "store", -]; - -export const useTranscriptColumnFilter = () => { - const excludedEventTypes = - useStore((state) => state.transcriptState.excludedTypes) || - kDefaultExcludedEventTypes; - - const setTranscriptState = useStore((state) => state.setTranscriptState); - - const setExcludedEventTypes = useCallback( - (newTypes: EventTypeValue[]) => { - setTranscriptState((prev) => ({ - ...prev, - excludedTypes: [...newTypes], - })); - }, - [setTranscriptState] - ); - - const toggleEventType = useCallback( - (type: EventTypeValue, isCurrentlyExcluded: boolean) => { - const newExcluded = new Set( - excludedEventTypes as EventTypeValue[] - ); - if (isCurrentlyExcluded) { - newExcluded.delete(type); - } else { - newExcluded.add(type); - } - - setExcludedEventTypes(Array.from(newExcluded)); - }, - [excludedEventTypes, setExcludedEventTypes] - ); - - const setDebugFilter = useCallback(() => { - setExcludedEventTypes([]); - }, [setExcludedEventTypes]); - - const setDefaultFilter = useCallback(() => { - setExcludedEventTypes([...kDefaultExcludedEventTypes]); - }, [setExcludedEventTypes]); - - const isDefaultFilter = useMemo(() => { - return ( - excludedEventTypes.length === kDefaultExcludedEventTypes.length && - excludedEventTypes.every((type) => - kDefaultExcludedEventTypes.includes(type as EventTypeValue) - ) - ); - }, [excludedEventTypes]); - - const isDebugFilter = useMemo(() => { - return excludedEventTypes.length === 0; - }, [excludedEventTypes]); - - const arrangedEventTypes = useCallback((columns: number = 1) => { - // Sort keys alphabetically with default disabled keys at the end - const sortedKeys = [...eventTypeValues].sort((a, b) => { - const aIsDefault = kDefaultExcludedEventTypes.includes(a); - const bIsDefault = kDefaultExcludedEventTypes.includes(b); - - // If one is in default exclude set and the other isn't, default goes to end - if (aIsDefault && !bIsDefault) return 1; - if (!aIsDefault && bIsDefault) return -1; - - // Both are in same category (both default or both not default), sort alphabetically - return a.localeCompare(b); - }); - - if (columns === 1) { - return sortedKeys; - } - - // Arrange for multi-column layout with proper reading order - const itemsPerColumn = Math.ceil(sortedKeys.length / columns); - const columnArrays: EventTypeValue[][] = []; - - // Split into columns - for (let col = 0; col < columns; col++) { - const start = col * itemsPerColumn; - const end = Math.min(start + itemsPerColumn, sortedKeys.length); - columnArrays.push(sortedKeys.slice(start, end)); - } - - // Interleave items from all columns - const arrangedKeys: EventTypeValue[] = []; - const maxItemsInColumn = Math.max(...columnArrays.map((col) => col.length)); - - for (let row = 0; row < maxItemsInColumn; row++) { - for (let col = 0; col < columns; col++) { - const item = columnArrays[col]?.[row]; - if (item !== undefined) { - arrangedKeys.push(item); - } - } - } - - return arrangedKeys; - }, []); - - return { - excludedEventTypes, - isDefaultFilter, - isDebugFilter, - setDefaultFilter, - setDebugFilter, - toggleEventType, - arrangedEventTypes, - }; -}; diff --git a/src/inspect_scout/_view/www/src/app/transcript/hooks/useTranscriptNavigation.ts b/src/inspect_scout/_view/www/src/app/transcript/hooks/useTranscriptNavigation.ts deleted file mode 100644 index e8ee99fa5..000000000 --- a/src/inspect_scout/_view/www/src/app/transcript/hooks/useTranscriptNavigation.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useCallback } from "react"; -import { useParams, useSearchParams } from "react-router-dom"; - -import { parseTranscriptParams, transcriptRoute } from "../../../router/url"; - -/** - * Converts a hash-router relative URL to a full absolute URL. - */ -const toFullUrl = (route: string): string => { - return `${window.location.origin}${window.location.pathname}#${route}`; -}; - -/** - * Hook for generating deep link URLs to specific events or messages - * within a transcript. - * - * @returns Functions to generate URLs for event and message deep links - */ -export const useTranscriptNavigation = () => { - const params = useParams<{ transcriptsDir: string; transcriptId: string }>(); - const { transcriptsDir, transcriptId } = parseTranscriptParams(params); - const [searchParams] = useSearchParams(); - - const getEventUrl = useCallback( - (eventId: string): string | undefined => { - if (!transcriptsDir || !transcriptId) return undefined; - const newParams = new URLSearchParams(searchParams); - newParams.set("tab", "transcript-events"); - newParams.set("event", eventId); - newParams.delete("message"); - return transcriptRoute(transcriptsDir, transcriptId, newParams); - }, - [transcriptsDir, transcriptId, searchParams] - ); - - const getMessageUrl = useCallback( - (messageId: string): string | undefined => { - if (!transcriptsDir || !transcriptId) return undefined; - const newParams = new URLSearchParams(searchParams); - newParams.set("tab", "transcript-messages"); - newParams.set("message", messageId); - newParams.delete("event"); - return transcriptRoute(transcriptsDir, transcriptId, newParams); - }, - [transcriptsDir, transcriptId, searchParams] - ); - - const getFullEventUrl = useCallback( - (eventId: string): string | undefined => { - const route = getEventUrl(eventId); - return route ? toFullUrl(route) : undefined; - }, - [getEventUrl] - ); - - const getFullMessageUrl = useCallback( - (messageId: string): string | undefined => { - const route = getMessageUrl(messageId); - return route ? toFullUrl(route) : undefined; - }, - [getMessageUrl] - ); - - return { getEventUrl, getMessageUrl, getFullEventUrl, getFullMessageUrl }; -}; diff --git a/src/inspect_scout/_view/www/src/app/transcripts/TranscriptFilterBar.tsx b/src/inspect_scout/_view/www/src/app/transcripts/TranscriptFilterBar.tsx deleted file mode 100644 index 0effc1795..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/TranscriptFilterBar.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { FC, useCallback } from "react"; - -import { ScalarValue } from "../../api/api"; -import { TranscriptsTableState, useStore } from "../../state/store"; -import type { TranscriptInfo } from "../../types/api-types"; -import { useAddFilterPopover } from "../components/columnFilter"; -import { FilterBar, type ColumnInfo } from "../components/FilterBar"; -import { useFilterBarHandlers } from "../components/useFilterBarHandlers"; - -import { - COLUMN_LABELS, - COLUMN_HEADER_TITLES, - DEFAULT_COLUMN_ORDER, - DEFAULT_VISIBLE_COLUMNS, - FILTER_COLUMNS, -} from "./columns"; - -// Convert column definitions to ColumnInfo array -const COLUMNS_INFO: ColumnInfo[] = DEFAULT_COLUMN_ORDER.map((key) => ({ - id: key, - label: COLUMN_LABELS[key], - headerTitle: COLUMN_HEADER_TITLES[key], -})); - -export const TranscriptFilterBar: FC<{ - filterCodeValues?: Record; - filterSuggestions?: ScalarValue[]; - onFilterColumnChange?: (columnId: string | null) => void; - includeColumnPicker?: boolean; -}> = ({ - filterCodeValues, - filterSuggestions = [], - onFilterColumnChange, - includeColumnPicker = true, -}) => { - // Transcript Filter State - const filters = useStore( - (state) => state.transcriptsTableState.columnFilters - ); - const visibleColumns = useStore( - (state) => state.transcriptsTableState.visibleColumns - ); - const setTranscriptsTableState = useStore( - (state) => state.setTranscriptsTableState - ); - - // Use shared filter bar handlers - const { handleFilterChange, removeFilter, handleAddFilter } = - useFilterBarHandlers({ - setTableState: setTranscriptsTableState, - defaultVisibleColumns: DEFAULT_VISIBLE_COLUMNS, - }); - - // Handle visible columns change - const handleVisibleColumnsChange = useCallback( - (newVisibleColumns: string[]) => { - setTranscriptsTableState((prevState) => ({ - ...prevState, - visibleColumns: newVisibleColumns as Array, - })); - }, - [setTranscriptsTableState] - ); - - // Add filter popover state - const addFilterPopover = useAddFilterPopover({ - columns: FILTER_COLUMNS, - filters, - onAddFilter: handleAddFilter, - onFilterColumnChange, - }); - - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/transcripts/TranscriptsGrid.tsx b/src/inspect_scout/_view/www/src/app/transcripts/TranscriptsGrid.tsx deleted file mode 100644 index e17c1ff61..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/TranscriptsGrid.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { FC, useEffect, useMemo, useRef } from "react"; - -import { ScalarValue } from "../../api/api"; -import { transcriptRoute } from "../../router/url"; -import { useStore } from "../../state/store"; -import { TranscriptInfo } from "../../types/api-types"; -import { DataGrid } from "../components/dataGrid"; - -import { - TranscriptColumn, - getTranscriptColumns, - DEFAULT_COLUMN_ORDER, -} from "./columns"; -import { useColumnSizing } from "./columnSizing"; - -// Default visible columns (stable reference) -const DEFAULT_VISIBLE_COLUMNS: Array = [ - "success", - "date", - "task_set", - "task_id", - "task_repeat", - "model", - "score", - "message_count", - "total_time", - "total_tokens", -]; - -// Generate a stable key for a transcript item -function transcriptItemKey(index: number, item?: TranscriptInfo): string { - if (!item) { - return String(index); - } - return `${item.source_uri}/${item.transcript_id}`; -} - -interface TranscriptGridProps { - transcripts: TranscriptInfo[]; - transcriptsDir?: string | null; - className?: string | string[]; - /** Called when scroll position nears end; receives distance from bottom in px. */ - onScrollNearEnd: (distanceFromBottom: number) => void; - /** Whether more data is available to fetch. */ - hasMore: boolean; - /** Distance from bottom (in px) at which to trigger callback. */ - fetchThreshold: number; - loading?: boolean; - /** Autocomplete suggestions for the currently editing filter column */ - filterSuggestions?: ScalarValue[]; - /** Called when a filter column starts/stops being edited */ - onFilterColumnChange?: (columnId: string | null) => void; -} - -export const TranscriptsGrid: FC = ({ - transcripts, - transcriptsDir, - className, - onScrollNearEnd, - hasMore, - fetchThreshold, - loading, - filterSuggestions = [], - onFilterColumnChange, -}) => { - // Table ref for DOM measurement (used by column sizing) - const tableRef = useRef(null); - - // Table state from store - const columnOrder = useStore( - (state) => state.transcriptsTableState.columnOrder - ); - const sorting = useStore((state) => state.transcriptsTableState.sorting); - const rowSelection = useStore( - (state) => state.transcriptsTableState.rowSelection - ); - const columnFilters = - useStore((state) => state.transcriptsTableState.columnFilters) ?? {}; - const focusedRowId = useStore( - (state) => state.transcriptsTableState.focusedRowId - ); - const visibleColumns = - useStore((state) => state.transcriptsTableState.visibleColumns) ?? - DEFAULT_VISIBLE_COLUMNS; - const setTableState = useStore((state) => state.setTranscriptsTableState); - - // Define table columns based on visible columns from store - const columns = useMemo( - () => getTranscriptColumns(visibleColumns), - [visibleColumns] - ); - - // Column sizing with min/max constraints and auto-sizing - const { - columnSizing, - handleColumnSizingChange, - applyAutoSizing, - resetColumnSize, - } = useColumnSizing({ - columns, - tableRef, - data: transcripts, - }); - - // Track if we've done initial auto-sizing - const hasInitializedRef = useRef(false); - - // Track previous visible columns to detect changes - const previousVisibleColumnsRef = useRef(null); - - // Auto-size columns on initial load when data is available - useEffect(() => { - if (!hasInitializedRef.current && transcripts.length > 0) { - hasInitializedRef.current = true; - applyAutoSizing(); - } - }, [transcripts.length, applyAutoSizing]); - - // Auto-size when visible columns change - // (applyAutoSizing preserves manually resized columns) - useEffect(() => { - const previousVisibleColumns = previousVisibleColumnsRef.current; - previousVisibleColumnsRef.current = visibleColumns; - - if (previousVisibleColumns && previousVisibleColumns !== visibleColumns) { - applyAutoSizing(); - } - }, [visibleColumns, applyAutoSizing]); - - // Compute effective column order: use explicit order if set, otherwise derive from DEFAULT_COLUMN_ORDER - const effectiveColumnOrder = useMemo(() => { - if (columnOrder && columnOrder.length > 0) { - return columnOrder; - } - // Filter DEFAULT_COLUMN_ORDER to only include visible columns - return DEFAULT_COLUMN_ORDER.filter((col) => visibleColumns.includes(col)); - }, [columnOrder, visibleColumns]); - - // Get row ID - const getRowId = (row: TranscriptInfo): string => String(row.transcript_id); - - // Get route for navigation - const getRowRoute = (row: TranscriptInfo): string => { - if (!transcriptsDir) return ""; - return transcriptRoute(transcriptsDir, String(row.transcript_id)); - }; - - return ( - - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/transcripts/TranscriptsPanel.module.css b/src/inspect_scout/_view/www/src/app/transcripts/TranscriptsPanel.module.css deleted file mode 100644 index 33f90bd7e..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/TranscriptsPanel.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.container { - height: 100%; - display: grid; - grid-template-rows: max-content max-content max-content 1fr max-content; -} - -.panel { - display: grid; - grid-template-columns: max-content 1fr; -} diff --git a/src/inspect_scout/_view/www/src/app/transcripts/TranscriptsPanel.tsx b/src/inspect_scout/_view/www/src/app/transcripts/TranscriptsPanel.tsx deleted file mode 100644 index ce1ed7f1a..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/TranscriptsPanel.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; -import clsx from "clsx"; -import { FC, useCallback, useEffect, useMemo } from "react"; - -import { ErrorPanel } from "../../components/ErrorPanel"; -import { LoadingBar } from "../../components/LoadingBar"; -import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import { useStore } from "../../state/store"; -import { TranscriptInfo } from "../../types/api-types"; -import { Footer } from "../components/Footer"; -import { TranscriptsNavbar } from "../components/TranscriptsNavbar"; -import { useTranscriptsFilterBarProps } from "../hooks/useTranscriptsFilterBarProps"; -import { useAppConfig } from "../server/useAppConfig"; -import { useServerTranscriptsInfinite } from "../server/useServerTranscriptsInfinite"; -import { useTranscriptsDir } from "../utils/useTranscriptsDir"; - -import { TRANSCRIPTS_INFINITE_SCROLL_CONFIG } from "./constants"; -import { TranscriptFilterBar } from "./TranscriptFilterBar"; -import { TranscriptsGrid } from "./TranscriptsGrid"; -import styles from "./TranscriptsPanel.module.css"; - -export const TranscriptsPanel: FC = () => { - useDocumentTitle("Transcripts"); - - // Resolve the active transcripts directory - const { - displayTranscriptsDir, - resolvedTranscriptsDir, - resolvedTranscriptsDirSource, - setTranscriptsDir, - } = useTranscriptsDir(); - const config = useAppConfig(); - const filter = Array.isArray(config.filter) - ? config.filter.join(" ") - : config.filter; - - const sorting = useStore((state) => state.transcriptsTableState.sorting); - - // Clear detail state - const clearTranscriptState = useStore((state) => state.clearTranscriptState); - useEffect(() => { - clearTranscriptState(); - }, [clearTranscriptState]); - - const { - filterCodeValues, - filterSuggestions, - onFilterColumnChange, - condition, - } = useTranscriptsFilterBarProps(resolvedTranscriptsDir); - const { data, error, fetchNextPage, hasNextPage, isFetching } = - useServerTranscriptsInfinite( - resolvedTranscriptsDir - ? { - location: resolvedTranscriptsDir, - pageSize: TRANSCRIPTS_INFINITE_SCROLL_CONFIG.pageSize, - filter: condition, - sorting, - } - : skipToken - ); - - const transcripts: TranscriptInfo[] = useMemo( - () => data?.pages.flatMap((page) => page.items) ?? [], - [data] - ); - - const handleScrollNearEnd = useCallback( - (distanceFromBottom: number) => { - if (distanceFromBottom <= 0) { - console.log("Hit bottom!"); - } - fetchNextPage({ cancelRefetch: false }).catch(console.error); - }, - [fetchNextPage] - ); - - return ( -
- - - {error && ( - - )} - {!error && ( - <> - - - - )} -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/index.ts b/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/index.ts deleted file mode 100644 index a963a7f1d..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from "./types"; -export * from "./useColumnSizing"; - -// Re-export shared utilities and strategies for convenience -export { - clampSize, - getColumnConstraints, - getColumnId, - getSizingStrategy, - sizingStrategies, -} from "../../components/columnSizing"; diff --git a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/strategies.test.ts b/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/strategies.test.ts deleted file mode 100644 index 4cf3ea250..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/strategies.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { TranscriptInfo } from "../../../types/api-types"; -import { - getSizingStrategy, - sizingStrategies, - SizingStrategy, -} from "../../components/columnSizing"; -import { TranscriptColumn } from "../columns"; - -describe("sizingStrategies", () => { - describe("default strategy", () => { - const strategy = sizingStrategies.default as SizingStrategy; - - it("extracts sizes from columns", () => { - const columns: TranscriptColumn[] = [ - { - accessorKey: "col1", - header: "Column 1", - size: 100, - } as TranscriptColumn, - { - accessorKey: "col2", - header: "Column 2", - size: 200, - } as TranscriptColumn, - ]; - - const sizes = strategy.computeSizes({ - tableElement: null, - columns, - data: [], - constraints: new Map(), - }); - - expect(sizes).toEqual({ - col1: 100, - col2: 200, - }); - }); - - it("skips columns without size", () => { - const columns: TranscriptColumn[] = [ - { - accessorKey: "col1", - header: "Column 1", - size: 100, - } as TranscriptColumn, - { - accessorKey: "col2", - header: "Column 2", - } as TranscriptColumn, - ]; - - const sizes = strategy.computeSizes({ - tableElement: null, - columns, - data: [], - constraints: new Map(), - }); - - expect(sizes).toEqual({ - col1: 100, - }); - expect(sizes.col2).toBeUndefined(); - }); - - it("returns empty object for empty columns", () => { - const sizes = strategy.computeSizes({ - tableElement: null, - columns: [], - data: [], - constraints: new Map(), - }); - expect(sizes).toEqual({}); - }); - }); - - describe("fit-content strategy", () => { - const strategy = sizingStrategies[ - "fit-content" - ] as SizingStrategy; - - it("falls back to default sizes when no table element", () => { - const columns: TranscriptColumn[] = [ - { - accessorKey: "col1", - header: "Column 1", - size: 100, - } as TranscriptColumn, - ]; - - const sizes = strategy.computeSizes({ - tableElement: null, - columns, - data: [], - constraints: new Map(), - }); - - expect(sizes).toEqual({ - col1: 100, - }); - }); - - it("falls back to DEFAULT_SIZE when column has no size", () => { - const columns: TranscriptColumn[] = [ - { - accessorKey: "col1", - header: "Column 1", - } as TranscriptColumn, - ]; - - const sizes = strategy.computeSizes({ - tableElement: null, - columns, - data: [], - constraints: new Map(), - }); - - expect(sizes).toEqual({ - col1: 150, // DEFAULT_SIZE - }); - }); - }); -}); - -describe("getSizingStrategy", () => { - it("returns default strategy for 'default' key", () => { - const strategy = getSizingStrategy("default"); - expect(strategy).toBe(sizingStrategies.default); - }); - - it("returns fit-content strategy for 'fit-content' key", () => { - const strategy = getSizingStrategy("fit-content"); - expect(strategy).toBe(sizingStrategies["fit-content"]); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/types.ts b/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/types.ts deleted file mode 100644 index da7773a2e..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Column sizing types for TranscriptsGrid. - * Re-exports shared types and provides transcript-specific type aliases. - */ - -// Re-export shared types and utilities -export { - DEFAULT_MAX_SIZE, - DEFAULT_MIN_SIZE, - DEFAULT_SIZE, -} from "../../components/columnSizing"; - -export type { - ColumnSizeConstraints, - ColumnSizingStrategyKey, -} from "../../components/columnSizing"; - -// Import for transcript-specific types -import { TranscriptInfo } from "../../../types/api-types"; -import { - SizingStrategy as GenericSizingStrategy, - SizingStrategyContext as GenericSizingStrategyContext, -} from "../../components/columnSizing"; -import { TranscriptColumn } from "../columns"; - -/** - * Context provided to sizing strategies for computing column sizes. - * Uses transcript-specific column and data types. - */ -export interface SizingStrategyContext extends Omit< - GenericSizingStrategyContext, - "columns" -> { - /** Column definitions */ - columns: TranscriptColumn[]; -} - -/** - * Interface for column sizing strategies. - * Each strategy computes column sizes differently. - */ -export type SizingStrategy = GenericSizingStrategy; diff --git a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/useColumnSizing.test.ts b/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/useColumnSizing.test.ts deleted file mode 100644 index 0ed7c2d12..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/useColumnSizing.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -// @vitest-environment jsdom -import { act, renderHook } from "@testing-library/react"; -import React from "react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import { TranscriptColumn } from "../columns"; - -import { useColumnSizing } from "./useColumnSizing"; - -// Mock the store -const mockSetTableState = vi.fn(); -const mockStoreState = { - transcriptsTableState: { - columnSizing: {}, - sizingStrategy: "default" as const, - manuallyResizedColumns: [] as string[], - }, -}; - -vi.mock("../../../state/store", () => ({ - useStore: vi.fn((selector: (state: typeof mockStoreState) => unknown) => - selector(mockStoreState) - ), -})); - -// Import useStore after mocking - eslint-disable required since mock must be defined first -// eslint-disable-next-line import/order -import { useStore } from "../../../state/store"; - -describe("useColumnSizing", () => { - const mockColumns: TranscriptColumn[] = [ - { - accessorKey: "col1", - header: "Column 1", - size: 100, - minSize: 50, - maxSize: 200, - } as TranscriptColumn, - { - accessorKey: "col2", - header: "Column 2", - size: 150, - minSize: 80, - maxSize: 300, - } as TranscriptColumn, - ]; - - const mockTableRef = { - current: null, - } as React.RefObject; - - const mockData: never[] = []; - - beforeEach(() => { - vi.clearAllMocks(); - - // Reset mock store state - mockStoreState.transcriptsTableState = { - columnSizing: {}, - sizingStrategy: "default", - manuallyResizedColumns: [], - }; - - // Setup useStore mock to return setTableState - (useStore as ReturnType).mockImplementation( - ( - selector: ( - state: typeof mockStoreState & { - setTranscriptsTableState: typeof mockSetTableState; - } - ) => unknown - ) => { - if (selector.toString().includes("setTranscriptsTableState")) { - return mockSetTableState; - } - return selector( - mockStoreState as typeof mockStoreState & { - setTranscriptsTableState: typeof mockSetTableState; - } - ); - } - ); - }); - - it("returns initial column sizing state", () => { - const { result } = renderHook(() => - useColumnSizing({ - columns: mockColumns, - tableRef: mockTableRef, - data: mockData, - }) - ); - - expect(result.current.columnSizing).toEqual({}); - expect(result.current.sizingStrategy).toBe("default"); - }); - - it("provides handleColumnSizingChange function", () => { - const { result } = renderHook(() => - useColumnSizing({ - columns: mockColumns, - tableRef: mockTableRef, - data: mockData, - }) - ); - - expect(typeof result.current.handleColumnSizingChange).toBe("function"); - }); - - it("provides setSizingStrategy function", () => { - const { result } = renderHook(() => - useColumnSizing({ - columns: mockColumns, - tableRef: mockTableRef, - data: mockData, - }) - ); - - expect(typeof result.current.setSizingStrategy).toBe("function"); - }); - - it("provides applyAutoSizing function", () => { - const { result } = renderHook(() => - useColumnSizing({ - columns: mockColumns, - tableRef: mockTableRef, - data: mockData, - }) - ); - - expect(typeof result.current.applyAutoSizing).toBe("function"); - }); - - it("provides resetColumnSizing function", () => { - const { result } = renderHook(() => - useColumnSizing({ - columns: mockColumns, - tableRef: mockTableRef, - data: mockData, - }) - ); - - expect(typeof result.current.clearColumnSizing).toBe("function"); - }); - - it("calls setTableState when setSizingStrategy is called", () => { - const { result } = renderHook(() => - useColumnSizing({ - columns: mockColumns, - tableRef: mockTableRef, - data: mockData, - }) - ); - - act(() => { - result.current.setSizingStrategy("fit-content"); - }); - - expect(mockSetTableState).toHaveBeenCalled(); - }); - - it("calls setTableState when resetColumnSizing is called", () => { - const { result } = renderHook(() => - useColumnSizing({ - columns: mockColumns, - tableRef: mockTableRef, - data: mockData, - }) - ); - - act(() => { - result.current.clearColumnSizing(); - }); - - expect(mockSetTableState).toHaveBeenCalled(); - }); - - it("calls setTableState when applyAutoSizing is called", () => { - const { result } = renderHook(() => - useColumnSizing({ - columns: mockColumns, - tableRef: mockTableRef, - data: mockData, - }) - ); - - act(() => { - result.current.applyAutoSizing(); - }); - - expect(mockSetTableState).toHaveBeenCalled(); - }); - - it("calls setTableState when handleColumnSizingChange is called", () => { - const { result } = renderHook(() => - useColumnSizing({ - columns: mockColumns, - tableRef: mockTableRef, - data: mockData, - }) - ); - - act(() => { - result.current.handleColumnSizingChange({ col1: 120 }); - }); - - expect(mockSetTableState).toHaveBeenCalled(); - }); - - it("handles function updater in handleColumnSizingChange", () => { - const { result } = renderHook(() => - useColumnSizing({ - columns: mockColumns, - tableRef: mockTableRef, - data: mockData, - }) - ); - - act(() => { - result.current.handleColumnSizingChange((prev) => ({ - ...prev, - col1: 120, - })); - }); - - expect(mockSetTableState).toHaveBeenCalled(); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/useColumnSizing.ts b/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/useColumnSizing.ts deleted file mode 100644 index 851c6a88c..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/useColumnSizing.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { ColumnSizingState, OnChangeFn } from "@tanstack/react-table"; -import { useCallback, useEffect, useMemo, useRef } from "react"; - -import { useStore } from "../../../state/store"; -import { TranscriptInfo } from "../../../types/api-types"; -import { - clampSize, - ColumnSizingStrategyKey, - getColumnConstraints, - getSizingStrategy, - SizingStrategy, -} from "../../components/columnSizing"; -import { TranscriptColumn } from "../columns"; - -interface UseColumnSizingOptions { - /** Column definitions */ - columns: TranscriptColumn[]; - /** Reference to the table element for DOM measurement */ - tableRef: React.RefObject; - /** Current data for content measurement */ - data: TranscriptInfo[]; -} - -interface UseColumnSizingResult { - /** Current column sizing state */ - columnSizing: ColumnSizingState; - /** Handler for column sizing changes with min/max enforcement */ - handleColumnSizingChange: OnChangeFn; - /** Current sizing strategy key */ - sizingStrategy: ColumnSizingStrategyKey; - /** Set the sizing strategy */ - setSizingStrategy: (strategy: ColumnSizingStrategyKey) => void; - /** Apply auto-sizing based on current strategy (preserves manually resized columns) */ - applyAutoSizing: () => void; - /** Reset a single column to its auto-calculated size */ - resetColumnSize: (columnId: string) => void; - /** Reset all column sizing and clear manual resize tracking */ - clearColumnSizing: () => void; -} - -/** - * Hook for managing column sizing with min/max constraints and auto-sizing. - * Manually resized columns are preserved during auto-sizing operations. - */ -export function useColumnSizing({ - columns, - tableRef, - data, -}: UseColumnSizingOptions): UseColumnSizingResult { - const columnSizing = useStore( - (state) => state.transcriptsTableState.columnSizing - ); - const sizingStrategy = useStore( - (state) => state.transcriptsTableState.sizingStrategy - ); - const manuallyResizedColumns = useStore( - (state) => state.transcriptsTableState.manuallyResizedColumns - ); - const setTableState = useStore((state) => state.setTranscriptsTableState); - - // Track which columns have been manually resized - const manuallyResizedSet = useMemo( - () => new Set(manuallyResizedColumns), - [manuallyResizedColumns] - ); - - // Get constraints for all columns - const columnConstraints = useMemo( - () => getColumnConstraints(columns), - [columns] - ); - - // Track if we're in the middle of an auto-sizing operation - const isAutoSizingRef = useRef(false); - - // Store latest values in refs for stable callbacks - const latestRef = useRef({ - sizingStrategy, - columns, - data, - columnConstraints, - manuallyResizedSet, - columnSizing, - }); - - // Update refs when values change - useEffect(() => { - latestRef.current = { - sizingStrategy, - columns, - data, - columnConstraints, - manuallyResizedSet, - columnSizing, - }; - }); - - // Handle column sizing changes with min/max enforcement - const handleColumnSizingChange: OnChangeFn = useCallback( - (updaterOrValue) => { - const newSizing = - typeof updaterOrValue === "function" - ? updaterOrValue(columnSizing) - : updaterOrValue; - - // Clamp sizes to min/max constraints - const clampedSizing: ColumnSizingState = {}; - const newManuallyResized = new Set(manuallyResizedSet); - - for (const [columnId, size] of Object.entries(newSizing)) { - const constraints = columnConstraints.get(columnId); - if (constraints) { - clampedSizing[columnId] = clampSize(size, constraints); - } else { - clampedSizing[columnId] = size; - } - - // Mark this column as manually resized (unless we're auto-sizing) - if (!isAutoSizingRef.current) { - newManuallyResized.add(columnId); - } - } - - // Include existing sizes that weren't updated - for (const [columnId, size] of Object.entries(columnSizing)) { - if (!(columnId in clampedSizing)) { - clampedSizing[columnId] = size; - } - } - - setTableState((prev) => ({ - ...prev, - columnSizing: clampedSizing, - manuallyResizedColumns: isAutoSizingRef.current - ? prev.manuallyResizedColumns - : Array.from(newManuallyResized), - })); - }, - [columnSizing, columnConstraints, manuallyResizedSet, setTableState] - ); - - // Set sizing strategy - const setSizingStrategy = useCallback( - (strategy: ColumnSizingStrategyKey) => { - setTableState((prev) => ({ - ...prev, - sizingStrategy: strategy, - })); - }, - [setTableState] - ); - - // Apply auto-sizing based on current strategy - // Preserves sizes of manually resized columns - const applyAutoSizing = useCallback(() => { - isAutoSizingRef.current = true; - - try { - const { - sizingStrategy: strategyKey, - columns: cols, - data: rowData, - columnConstraints: constraints, - manuallyResizedSet: resizedSet, - columnSizing: currentSizing, - } = latestRef.current; - - const strategy = getSizingStrategy( - strategyKey - ) as SizingStrategy; - const calculatedSizing = strategy.computeSizes({ - tableElement: tableRef.current, - columns: cols, - data: rowData, - constraints, - }); - - // Merge: use calculated sizes for non-manually-resized columns, - // preserve existing sizes for manually resized columns - const newSizing: ColumnSizingState = {}; - for (const [columnId, size] of Object.entries(calculatedSizing)) { - if (resizedSet.has(columnId) && currentSizing[columnId] !== undefined) { - // Preserve manually resized column's current size - newSizing[columnId] = currentSizing[columnId]; - } else { - // Use calculated size - newSizing[columnId] = size; - } - } - - setTableState((prev) => ({ - ...prev, - columnSizing: newSizing, - })); - } finally { - isAutoSizingRef.current = false; - } - }, [tableRef, setTableState]); - - // Reset a single column to its auto-calculated size - const resetColumnSize = useCallback( - (columnId: string) => { - isAutoSizingRef.current = true; - - try { - const { - sizingStrategy: strategyKey, - columns: cols, - data: rowData, - columnConstraints: constraints, - } = latestRef.current; - - const strategy = getSizingStrategy( - strategyKey - ) as SizingStrategy; - const allSizes = strategy.computeSizes({ - tableElement: tableRef.current, - columns: cols, - data: rowData, - constraints, - }); - - const newSize = allSizes[columnId]; - if (newSize !== undefined) { - setTableState((prev) => { - // Remove this column from manually resized list - const newManuallyResized = prev.manuallyResizedColumns.filter( - (id) => id !== columnId - ); - - return { - ...prev, - columnSizing: { - ...prev.columnSizing, - [columnId]: newSize, - }, - manuallyResizedColumns: newManuallyResized, - }; - }); - } - } finally { - isAutoSizingRef.current = false; - } - }, - [tableRef, setTableState] - ); - - // Reset all column sizing and clear manual resize tracking - const clearColumnSizing = useCallback(() => { - setTableState((prev) => ({ - ...prev, - columnSizing: {}, - manuallyResizedColumns: [], - })); - }, [setTableState]); - - return { - columnSizing, - setSizingStrategy, - clearColumnSizing, - sizingStrategy, - handleColumnSizingChange, - applyAutoSizing, - resetColumnSize, - }; -} diff --git a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/utils.test.ts b/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/utils.test.ts deleted file mode 100644 index e3d11609c..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/columnSizing/utils.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - clampSize, - getColumnConstraints, - getColumnId, -} from "../../components/columnSizing"; -import { TranscriptColumn } from "../columns"; - -describe("clampSize", () => { - const constraints = { size: 150, minSize: 50, maxSize: 200 }; - - it("returns size unchanged when within constraints", () => { - expect(clampSize(100, constraints)).toBe(100); - }); - - it("returns size at minSize boundary", () => { - expect(clampSize(50, constraints)).toBe(50); - }); - - it("returns size at maxSize boundary", () => { - expect(clampSize(200, constraints)).toBe(200); - }); - - it("clamps to minSize when below minimum", () => { - expect(clampSize(30, constraints)).toBe(50); - }); - - it("clamps to maxSize when above maximum", () => { - expect(clampSize(300, constraints)).toBe(200); - }); - - it("clamps negative values to minSize", () => { - expect(clampSize(-10, constraints)).toBe(50); - }); -}); - -describe("getColumnConstraints", () => { - it("extracts constraints from columns with all properties", () => { - const columns: TranscriptColumn[] = [ - { - accessorKey: "test", - header: "Test", - size: 100, - minSize: 50, - maxSize: 200, - } as TranscriptColumn, - ]; - - const constraints = getColumnConstraints(columns); - - expect(constraints.get("test")).toEqual({ - size: 100, - minSize: 50, - maxSize: 200, - }); - }); - - it("uses default values when constraints not specified", () => { - const columns: TranscriptColumn[] = [ - { - accessorKey: "test", - header: "Test", - } as TranscriptColumn, - ]; - - const constraints = getColumnConstraints(columns); - - expect(constraints.get("test")).toEqual({ - size: 150, // DEFAULT_SIZE - minSize: 40, // DEFAULT_MIN_SIZE - maxSize: 600, // DEFAULT_MAX_SIZE - }); - }); - - it("uses column id when accessorKey not available", () => { - const columns: TranscriptColumn[] = [ - { - id: "custom-id", - header: "Test", - size: 120, - } as TranscriptColumn, - ]; - - const constraints = getColumnConstraints(columns); - - expect(constraints.has("custom-id")).toBe(true); - expect(constraints.get("custom-id")?.size).toBe(120); - }); - - it("handles multiple columns", () => { - const columns: TranscriptColumn[] = [ - { - accessorKey: "col1", - header: "Column 1", - size: 100, - minSize: 60, - maxSize: 200, - } as TranscriptColumn, - { - accessorKey: "col2", - header: "Column 2", - size: 150, - minSize: 80, - maxSize: 300, - } as TranscriptColumn, - ]; - - const constraints = getColumnConstraints(columns); - - expect(constraints.size).toBe(2); - expect(constraints.get("col1")).toEqual({ - size: 100, - minSize: 60, - maxSize: 200, - }); - expect(constraints.get("col2")).toEqual({ - size: 150, - minSize: 80, - maxSize: 300, - }); - }); - - it("returns empty map for empty columns array", () => { - const constraints = getColumnConstraints([]); - expect(constraints.size).toBe(0); - }); -}); - -describe("getColumnId", () => { - it("returns accessorKey when available", () => { - const column = { - accessorKey: "test_key", - header: "Test", - } as TranscriptColumn; - - expect(getColumnId(column)).toBe("test_key"); - }); - - it("returns id when accessorKey not available", () => { - const column = { - id: "custom_id", - header: "Test", - } as TranscriptColumn; - - expect(getColumnId(column)).toBe("custom_id"); - }); - - it("prefers id over accessorKey when both present", () => { - const column = { - id: "custom_id", - accessorKey: "accessor_key", - header: "Test", - } as TranscriptColumn; - - // id takes precedence since it's checked first - expect(getColumnId(column)).toBe("custom_id"); - }); - - it("returns empty string when neither id nor accessorKey present", () => { - const column = { - header: "Test", - } as TranscriptColumn; - - expect(getColumnId(column)).toBe(""); - }); -}); diff --git a/src/inspect_scout/_view/www/src/app/transcripts/columns.module.css b/src/inspect_scout/_view/www/src/app/transcripts/columns.module.css deleted file mode 100644 index 6cdfa171d..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/columns.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.success { - color: var(--bs-success) !important; -} - -.unsuccess { - opacity: 0.5 !important; -} diff --git a/src/inspect_scout/_view/www/src/app/transcripts/columns.tsx b/src/inspect_scout/_view/www/src/app/transcripts/columns.tsx deleted file mode 100644 index 06d4d01ad..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/columns.tsx +++ /dev/null @@ -1,657 +0,0 @@ -import { ColumnDef } from "@tanstack/react-table"; -import clsx from "clsx"; - -import { ApplicationIcons } from "../../components/icons"; -import { FilterType } from "../../state/store"; -import { TranscriptInfo } from "../../types/api-types"; -import { printArray } from "../../utils/array"; -import { - formatNumber, - formatPrettyDecimal, - formatTime, -} from "../../utils/format"; -import { printObject } from "../../utils/object"; -import type { AvailableColumn } from "../components/columnFilter"; - -import styles from "./columns.module.css"; - -// Column headers for display (used in column picker and add filter dropdown) -export const COLUMN_LABELS: Record = { - success: "Success", - date: "Date", - transcript_id: "Transcript ID", - task_set: "Task Set", - task_id: "Task ID", - task_repeat: "Repeat", - model: "Model", - model_options: "Model Options", - agent: "Agent", - agent_args: "Agent Args", - score: "Score", - metadata: "Metadata", - source_id: "Source ID", - source_type: "Source Type", - source_uri: "Source URI", - total_tokens: "Total Tokens", - total_time: "Total Time", - message_count: "Messages", - limit: "Limit", - error: "Error", -}; - -// Column header tooltips (matches headerTitle in column definitions) -export const COLUMN_HEADER_TITLES: Record = { - success: "Boolean reduction of score to succeeded/failed.", - date: "The date and time when the transcript was created.", - transcript_id: - "Globally unique identifier for a transcript (maps to EvalSample.uuid in Inspect logs).", - task_set: - "Set from which transcript task was drawn (e.g. Inspect task name or benchmark name)", - task_id: "Identifier for task (e.g. dataset sample id).", - task_repeat: "Repeat for a given task id within a task set (e.g. epoch).", - model: "Main model used by agent.", - model_options: "Generation options for main model.", - agent: "Agent used to to execute task.", - agent_args: "Arguments passed to create agent.", - score: "Value indicating score on task.", - metadata: - "Transcript source specific metadata (e.g. model, task name, errors, epoch, dataset sample id, limits, etc.).", - source_id: - "Globally unique identifier for a transcript source (maps to eval_id in Inspect logs)", - source_type: 'Type of transcript source (e.g. "eval_log", "weave", etc.).', - source_uri: - "Globally unique identifier for a transcript source (maps to eval_id in Inspect logs)", - total_tokens: "Tokens spent in execution of task.", - total_time: "Time required to execute task (seconds).", - message_count: "Total messages in conversation.", - limit: - 'Limit that caused the task to exit (e.g. "tokens", "messages", etc.).', - error: "Error message that terminated the task.", -}; - -export type TranscriptColumn = ColumnDef & { - meta?: { - align?: "left" | "center" | "right"; - filterable?: boolean; - filterType?: FilterType; - }; - /** Returns string for tooltip display */ - titleValue?: (value: unknown) => string; - /** Returns string representation for column width measurement. Return null to skip content measurement. */ - textValue?: (value: unknown) => string | null; - /** Minimum column width in pixels */ - minSize?: number; - /** Maximum column width in pixels */ - maxSize?: number; - /** Tooltip text for the column header */ - headerTitle?: string; -}; - -// Helper to create strongly-typed columns -function createColumn(config: { - accessorKey: K; - header: string; - headerTitle?: string; - size?: number; - minSize?: number; - maxSize?: number; - meta?: { - align?: "left" | "center" | "right"; - filterable?: boolean; - filterType?: FilterType; - }; - cell?: (value: TranscriptInfo[K]) => React.ReactNode; - titleValue?: (value: TranscriptInfo[K]) => string; - textValue?: (value: TranscriptInfo[K]) => string | null; -}): TranscriptColumn { - // Default textValue: convert to string - const defaultTextValue = (value: unknown): string => { - if (value === undefined || value === null) { - return "-"; - } - return String(value); - }; - - return { - accessorKey: config.accessorKey as string, - header: config.header, - headerTitle: config.headerTitle, - size: config.size, - minSize: config.minSize, - maxSize: config.maxSize, - meta: config.meta, - titleValue: config.titleValue as ((value: unknown) => string) | undefined, - textValue: config.textValue - ? (config.textValue as (value: unknown) => string | null) - : defaultTextValue, - cell: (info) => { - const value = info.getValue() as TranscriptInfo[K]; - if (config.cell) { - return config.cell(value); - } - if (value === undefined || value === null) { - return "-"; - } - return String(value); - }, - }; -} - -// Helper to create columns that display JSON objects with truncated display and full tooltip -function createObjectColumn(config: { - accessorKey: K; - header: string; - headerTitle?: string; - size?: number; - minSize?: number; - maxSize?: number; - meta?: { - align?: "left" | "center" | "right"; - filterable?: boolean; - filterType?: FilterType; - }; - maxDisplayLength?: number; -}): TranscriptColumn { - const maxLength = config.maxDisplayLength ?? 1000; - - const formatObjectValue = (value: TranscriptInfo[K]): string => { - if (!value) { - return "-"; - } - try { - if (typeof value === "object") { - return printObject(value, maxLength); - } - return String(value); - } catch { - return String(value); - } - }; - - return createColumn({ - accessorKey: config.accessorKey, - header: config.header, - headerTitle: config.headerTitle, - size: config.size, - minSize: config.minSize, - maxSize: config.maxSize, - meta: config.meta, - cell: formatObjectValue, - textValue: formatObjectValue, - titleValue: (value) => { - if (!value) { - return ""; - } - if (typeof value === "object") { - return JSON.stringify(value, null, 2); - } - return String(value); - }, - }); -} - -// All available columns, keyed by their accessor key -export const ALL_COLUMNS: Record = { - success: createColumn({ - accessorKey: "success", - header: "✓", - headerTitle: "Boolean reduction of score to succeeded/failed.", - size: 40, - minSize: 40, - maxSize: 60, - meta: { - align: "center", - filterable: true, - filterType: "boolean", - }, - cell: (value) => { - if (value === undefined || value === null) { - return "-"; - } - - const icon = value - ? ApplicationIcons.checkbox.checked - : ApplicationIcons.checkbox.unchecked; - - return ( - - ); - }, - textValue: () => null, - }), - date: createColumn({ - accessorKey: "date", - header: "Date", - headerTitle: "The date and time when the transcript was created.", - size: 180, - minSize: 120, - maxSize: 300, - meta: { - filterable: true, - filterType: "date", - }, - cell: (value) => { - if (!value) { - return "-"; - } - const date = new Date(value); - return date.toLocaleString(); - }, - textValue: (value) => { - if (!value) { - return "-"; - } - const date = new Date(value as string | number); - return date.toLocaleString(); - }, - }), - transcript_id: createColumn({ - accessorKey: "transcript_id", - header: "Transcript ID", - headerTitle: - "Globally unique identifier for a transcript (maps to EvalSample.uuid in Inspect logs).", - size: 150, - minSize: 80, - maxSize: 400, - meta: { - filterable: true, - filterType: "string", - }, - cell: (value) => { - return value || "-"; - }, - }), - task_set: createColumn({ - accessorKey: "task_set", - header: "Task Set", - headerTitle: - "Set from which transcript task was drawn (e.g. Inspect task name or benchmark name)", - size: 150, - minSize: 80, - maxSize: 400, - meta: { - filterable: true, - filterType: "string", - }, - }), - task_id: createColumn({ - accessorKey: "task_id", - header: "Task ID", - headerTitle: "Identifier for task (e.g. dataset sample id).", - size: 150, - minSize: 80, - maxSize: 400, - meta: { - filterable: true, - filterType: "string", - }, - }), - task_repeat: createColumn({ - accessorKey: "task_repeat", - header: "#", - headerTitle: "Repeat for a given task id within a task set (e.g. epoch).", - size: 50, - minSize: 40, - maxSize: 100, - meta: { - filterable: true, - filterType: "number", - }, - }), - model: createColumn({ - accessorKey: "model", - header: "Model", - headerTitle: "Main model used by agent.", - size: 200, - minSize: 80, - maxSize: 400, - meta: { - filterable: true, - filterType: "string", - }, - }), - model_options: createObjectColumn({ - accessorKey: "model_options", - header: "Model Options", - headerTitle: "Generation options for main model.", - size: 200, - minSize: 80, - maxSize: 500, - meta: { - filterable: true, - filterType: "string", - }, - }), - agent: createColumn({ - accessorKey: "agent", - header: "Agent", - headerTitle: "Agent used to to execute task.", - size: 150, - minSize: 80, - maxSize: 400, - meta: { - filterable: true, - filterType: "string", - }, - cell: (agent) => { - return agent || "-"; - }, - }), - agent_args: createObjectColumn({ - accessorKey: "agent_args", - header: "Agent Args", - headerTitle: "Arguments passed to create agent.", - size: 200, - minSize: 80, - maxSize: 500, - meta: { - filterable: true, - filterType: "string", - }, - }), - score: createColumn({ - accessorKey: "score", - header: "Score", - headerTitle: "Value indicating score on task.", - size: 100, - minSize: 60, - maxSize: 300, - meta: { - filterable: true, - filterType: "string", - }, - cell: (value) => { - if (!value) { - return "-"; - } - - if (Array.isArray(value)) { - return printArray(value, 1000); - } else if (typeof value === "object") { - return printObject(value, 1000); - } else if (typeof value === "number") { - return formatPrettyDecimal(value); - } else { - return String(value); - } - }, - textValue: (value) => { - if (!value) { - return "-"; - } - if (Array.isArray(value)) { - return printArray(value, 1000); - } else if (typeof value === "object") { - return printObject(value as Record, 1000); - } else if (typeof value === "number") { - return formatPrettyDecimal(value); - } else { - return String(value); - } - }, - titleValue: (value) => { - if (!value) { - return ""; - } - if (Array.isArray(value) || typeof value === "object") { - return JSON.stringify(value, null, 2); - } - return String(value); - }, - }), - metadata: createObjectColumn({ - accessorKey: "metadata", - header: "Metadata", - headerTitle: - "Transcript source specific metadata (e.g. model, task name, errors, epoch, dataset sample id, limits, etc.).", - size: 200, - minSize: 80, - maxSize: 500, - meta: { - filterable: true, - filterType: "string", - }, - }), - source_id: createColumn({ - accessorKey: "source_id", - header: "Source ID", - headerTitle: - "Globally unique identifier for a transcript source (maps to eval_id in Inspect logs)", - size: 150, - minSize: 80, - maxSize: 400, - meta: { - filterable: true, - filterType: "string", - }, - cell: (value) => { - return value || "-"; - }, - }), - source_type: createColumn({ - accessorKey: "source_type", - header: "Source Type", - headerTitle: "Type of transcript source (e.g. “eval_log”, “weave”, etc.).", - size: 150, - minSize: 80, - maxSize: 300, - meta: { - filterable: true, - filterType: "string", - }, - cell: (value) => { - return value || "-"; - }, - }), - source_uri: createColumn({ - accessorKey: "source_uri", - header: "Source URI", - headerTitle: - "Globally unique identifier for a transcript source (maps to eval_id in Inspect logs)", - size: 300, - minSize: 100, - maxSize: 600, - meta: { - filterable: true, - filterType: "string", - }, - cell: (value) => { - return value || "-"; - }, - }), - total_tokens: createColumn({ - accessorKey: "total_tokens", - header: "Tokens", - headerTitle: "Tokens spent in execution of task.", - size: 120, - minSize: 60, - maxSize: 200, - meta: { - filterable: true, - filterType: "number", - }, - cell: (value) => { - if (value == null) { - return "-"; - } - return formatNumber(value); - }, - textValue: (value) => { - if (value == null) { - return "-"; - } - return formatNumber(value); - }, - }), - total_time: createColumn({ - accessorKey: "total_time", - header: "Time", - headerTitle: "Time required to execute task (seconds).", - size: 120, - minSize: 60, - maxSize: 200, - meta: { - filterable: true, - filterType: "duration", - }, - cell: (value) => { - if (value == null) { - return "-"; - } - return formatTime(value); - }, - textValue: (value) => { - if (value == null) { - return "-"; - } - return formatTime(value); - }, - }), - message_count: createColumn({ - accessorKey: "message_count", - header: "Messages", - headerTitle: "Total messages in conversation.", - size: 120, - minSize: 60, - maxSize: 200, - meta: { - filterable: true, - filterType: "number", - }, - cell: (value) => { - if (value == null) { - return "-"; - } - return formatNumber(value); - }, - textValue: (value) => { - if (value == null) { - return "-"; - } - return formatNumber(value); - }, - }), - limit: createColumn({ - accessorKey: "limit", - header: "Limit", - headerTitle: - "Limit that caused the task to exit (e.g. “tokens”, “messages, etc.).", - size: 100, - minSize: 60, - maxSize: 200, - meta: { - filterable: true, - filterType: "string", - }, - cell: (value) => { - return value || "-"; - }, - }), - error: createColumn({ - accessorKey: "error", - header: "Error", - headerTitle: "Error message that terminated the task.", - size: 200, - minSize: 80, - maxSize: 500, - meta: { - filterable: true, - filterType: "string", - }, - cell: (value) => { - return value || "-"; - }, - }), -}; - -// Default column order (matches current order in TranscriptsGrid) -export const DEFAULT_COLUMN_ORDER: Array = [ - "success", - "date", - "transcript_id", - "task_set", - "task_id", - "task_repeat", - "model", - "model_options", - "agent", - "agent_args", - "score", - "metadata", - "source_id", - "source_type", - "source_uri", - "total_tokens", - "total_time", - "message_count", - "limit", - "error", -]; - -// Default visible columns -export const DEFAULT_VISIBLE_COLUMNS: Array = [ - "success", - "date", - "task_set", - "task_id", - "task_repeat", - "model", - "score", - "message_count", - "total_time", - "total_tokens", -]; - -/** - * Get columns for the TranscriptsGrid. - * @param visibleColumnKeys - Optional list of column keys to display. If not provided, returns all columns in default order. - * @returns Array of column definitions in the order specified or default order. - */ -export function getTranscriptColumns( - visibleColumnKeys?: Array -): TranscriptColumn[] { - if (!visibleColumnKeys) { - return DEFAULT_COLUMN_ORDER.map((key) => ALL_COLUMNS[key]); - } - - return visibleColumnKeys.map((key) => ALL_COLUMNS[key]); -} - -/** - * Extract title value for tooltip from a cell. - */ -export function getCellTitleValue( - cell: any, - columnDef: TranscriptColumn -): string { - const value = cell.getValue(); - - // Use custom titleValue function if provided - if (columnDef.titleValue) { - return columnDef.titleValue(value); - } - - // Default fallback - if (value === undefined || value === null) { - return ""; - } - if (typeof value === "object") { - return JSON.stringify(value, null, 2); - } - return String(value); -} - -// Columns available for filtering (used by Add Filter popover) -export const FILTER_COLUMNS: AvailableColumn[] = DEFAULT_COLUMN_ORDER.map( - (columnId) => ({ - id: columnId, - label: COLUMN_LABELS[columnId], - filterType: ALL_COLUMNS[columnId].meta?.filterType ?? "string", - }) -); diff --git a/src/inspect_scout/_view/www/src/app/transcripts/constants.ts b/src/inspect_scout/_view/www/src/app/transcripts/constants.ts deleted file mode 100644 index 3e211ad17..000000000 --- a/src/inspect_scout/_view/www/src/app/transcripts/constants.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Infinite Scroll Tuning - * - * Goal: user never hits bottom while waiting for next page. - * - * Formula: threshold >= scroll_speed × fetch_duration - * - * Assumptions: - * row_height = 29px - * fetch_duration = 300-1000ms (variable with fixed overhead) - * max_scroll_speed = 1500px/s (typical fast scroller) - * - * Check at typical speed (1500px/s): - * runway_time = 2000px / 1500px/s = 1333ms - * worst_case_fetch = 1000ms - * margin = 333ms ✓ - * - * Check at extreme speed (5000px/s): - * runway_time = 2000px / 5000px/s = 400ms - * median_fetch = ~350ms - * margin = 50ms (tight but ok) ✓ - * - * Why large pageSize? Fetch duration is mostly fixed overhead, so larger - * pages = fewer fetches = fewer stall opportunities. 500 rows gives ~9.7s - * of scrolling per page at 1500px/s. - * - * Note: If threshold > pageSize_px, the next page is prefetched immediately - * after the current page loads. This is fine for maximum smoothness. - */ -export const TRANSCRIPTS_INFINITE_SCROLL_CONFIG = { - /** Number of rows to fetch per page (500 rows = 14,500px at 29px/row) */ - pageSize: 500, - /** Distance from bottom (in px) at which to trigger fetch (~69 rows) */ - threshold: 2000, -} as const; diff --git a/src/inspect_scout/_view/www/src/app/types.ts b/src/inspect_scout/_view/www/src/app/types.ts deleted file mode 100644 index 2e42c057b..000000000 --- a/src/inspect_scout/_view/www/src/app/types.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { EventType } from "../components/transcript/types"; -import { - ModelUsage, - JsonValue, - ChatMessageSystem, - ChatMessageUser, - ChatMessageAssistant, - ChatMessageTool, - Event, - ChatMessage, - Transcript, -} from "../types/api-types"; - -export interface ScanResultInputData { - input: Input; - inputType: InputType; -} - -export type Input = - | Transcript - | ChatMessage[] - | Event[] - | MessageType - | EventType; - -export type InputType = - | "transcript" - | "message" - | "messages" - | "event" - | "events"; - -export interface ScanResultSummary { - // Basic Info - identifier: string; - // The original DB result UUID. Shared across expanded resultset rows (e.g. - // multiple labels from one scan result will have the same uuid but different - // identifiers). Used to fetch the shared input data for the result. - uuid?: string; - explanation?: string; - label?: string; - timestamp?: string; - - // Input - inputType: InputType; - - // Refs - eventReferences: ScanResultReference[]; - messageReferences: ScanResultReference[]; - - // Validation - validationResult: boolean | Record; - validationTarget: JsonValue; - - // Value - value: string | boolean | number | null | unknown[] | object; - valueType: ValueType; - - // Scan metadata - scanError?: string; - scanErrorRefusal?: boolean; - - // Transcript info - transcriptSourceId: string; - transcriptTaskSet?: string; - transcriptTaskId?: string | number; - transcriptTaskRepeat?: number; - transcriptModel?: string; - transcriptMetadata: Record; -} - -// Base interface with common properties -export interface ScanResultData extends ScanResultSummary { - answer?: string; - inputIds: string[]; - metadata: Record; - scanError?: string; - scanErrorTraceback?: string; - scanErrorRefusal?: boolean; - scanEvents: Event[]; - scanId: string; - scanMetadata: Record; - scanModelUsage: Record; - scanTags: string[]; - scanTotalTokens: number; - scannerFile: string; - scannerKey: string; - scannerName: string; - scannerParams: Record; - transcriptId: string; - transcriptSourceUri: string; - - transcriptDate?: string; - transcriptAgent?: string; - transcriptAgentArgs?: Record; - transcriptScore?: JsonValue; - transcriptSuccess?: boolean; - transcriptMessageCount?: number; - transcriptTotalTime?: number; - transcriptTotalTokens?: number; - transcriptError?: string; - transcriptLimit?: string; -} - -export interface ScanResultReference { - type: "message" | "event"; - id: string; - cite?: string; -} - -export type MessageType = - | ChatMessageSystem - | ChatMessageUser - | ChatMessageAssistant - | ChatMessageTool; - -export interface SortColumn { - column: string; - direction: "asc" | "desc"; -} - -export type ErrorScope = - | "scans" - | "scanner" - | "dataframe" - | "dataframe_input" - | "transcripts"; - -export type ResultGroup = - | "source" - | "label" - | "id" - | "epoch" - | "model" - | "none"; - -export type ValueType = - | "boolean" - | "number" - | "string" - | "array" - | "object" - | "null"; - -// Type guard functions for value types -export function isStringValue( - result: ScanResultSummary -): result is ScanResultSummary & { valueType: "string"; value: string } { - return result.valueType === "string"; -} - -export function isNumberValue( - result: ScanResultSummary -): result is ScanResultSummary & { valueType: "number"; value: number } { - return result.valueType === "number"; -} - -export function isBooleanValue( - result: ScanResultSummary -): result is ScanResultSummary & { valueType: "boolean"; value: boolean } { - return result.valueType === "boolean"; -} - -export function isNullValue( - result: ScanResultSummary -): result is ScanResultSummary & { valueType: "null"; value: null } { - return result.valueType === "null"; -} - -export function isArrayValue( - result: ScanResultSummary -): result is ScanResultSummary & { valueType: "array"; value: unknown[] } { - return result.valueType === "array"; -} - -export function isObjectValue( - result: ScanResultSummary -): result is ScanResultSummary & { valueType: "object"; value: object } { - return result.valueType === "object"; -} - -// Type guard functions for DataFrameInput -export function isTranscriptInput( - input: ScanResultInputData -): input is ScanResultInputData & { - inputType: "transcript"; - input: Transcript; -} { - return input.inputType === "transcript"; -} - -export function isMessageInput( - input: ScanResultInputData -): input is ScanResultInputData & { inputType: "message"; input: MessageType } { - return input.inputType === "message"; -} - -export function isMessagesInput( - input: ScanResultInputData -): input is ScanResultInputData & { - inputType: "messages"; - input: ChatMessage[]; -} { - return input.inputType === "messages"; -} - -export function isEventInput( - input: ScanResultInputData -): input is ScanResultInputData & { inputType: "event"; input: EventType } { - return input.inputType === "event"; -} - -export function isEventsInput( - input: ScanResultInputData -): input is ScanResultInputData & { inputType: "events"; input: Event[] } { - return input.inputType === "events"; -} diff --git a/src/inspect_scout/_view/www/src/app/utils/arrow.ts b/src/inspect_scout/_view/www/src/app/utils/arrow.ts deleted file mode 100644 index 59e283f75..000000000 --- a/src/inspect_scout/_view/www/src/app/utils/arrow.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { ColumnTable, from } from "arquero"; -import JSON5 from "json5"; - -import { asyncJsonParse } from "../../utils/json-worker"; -import { ScanResultReference, ValueType } from "../types"; - -interface Result { - uuid?: string | null; - label?: string | null; - value: unknown; - type?: ValueType | null; - answer?: string | null; - explanation?: string | null; - metadata?: Record | null; - references?: ScanResultReference[]; -} - -// Expand rows where value_type == "resultset" into multiple rows. -// -// For rows with value_type == "resultset", the value field contains a JSON-encoded -// list of Result objects. This function: -// 1. Parses the JSON value into a list -// 2. Explodes each list element into its own row using Arquero's unroll() -// 3. Normalizes the Result fields into columns (uuid, label, value, etc.) -// 4. Applies type casting to the expanded value column -// -// I tested an alternative approach to this using Arquero's unroll() function -// directly in a derive() expression, but it wasn't faster (was actually a -// touch slower anecdotally) and was a much more complex set of operations. -// I omit that function and instead just operate on the rows directly. -export async function expandResultsetRows( - columnTable: ColumnTable -): Promise { - // Ensure that each row in the table has an "identifier" column (this is used as a unique key for - // referencing rows since code below may expand one row with a resultset into multiple rows. - // We use the existing "uuid" column if it exists, otherwise we generate a random UUID. It is expected that the resultset expansion - // will overwrite this value for expanded rows - const colNames = columnTable.columnNames(); - if (!colNames.includes("identifier")) { - const numRows = columnTable.numRows(); - const identifiers = new Array(numRows); - if (colNames.includes("uuid")) { - const uuids = columnTable.array("uuid") as (string | null | undefined)[]; - for (let i = 0; i < numRows; i++) { - identifiers[i] = uuids[i] ?? crypto.randomUUID(); - } - } else { - for (let i = 0; i < numRows; i++) { - identifiers[i] = crypto.randomUUID(); - } - } - columnTable = columnTable.assign({ identifier: identifiers }); - } - - // Check if we have any resultset rows - if ( - !colNames.includes("value_type") || - !colNames.includes("value") || - columnTable.numRows() === 0 - ) { - return columnTable; - } - - // Are there any results sets to explode? - const resultsetCount = columnTable - .filter((d: { value_type: string }) => d.value_type === "resultset") - .numRows(); - if (resultsetCount === 0) { - // No result sets - return columnTable; - } - - // Split into resultset and non-resultset rows - const resultsetRows = columnTable.filter( - (d: { value_type: string }) => d.value_type === "resultset" - ); - const otherRows = columnTable.filter( - (d: { value_type: string }) => d.value_type !== "resultset" - ); - - // Parse JSON value strings and expand into multiple rows - // (Arquero doesn't support try-catch in derive expressions, so we do this in plain JS) - const resultObjs = resultsetRows.objects() as Record[]; - const explodedResultsetRows: Record[] = []; - - for (const row of resultObjs) { - try { - // Get the result set value - const valueStr = row.value as string; - const results = valueStr ? JSON5.parse(valueStr) : []; - - // If the row has an empty result set, just leave it - // intact - if (!results || results.length === 0) { - const expandedRow = { ...row }; - expandedRow.value = null; - expandedRow.value_type = "null"; - explodedResultsetRows.push(expandedRow); - continue; - } - - for (const result of results) { - const expandedRow = { ...row }; - - // Record the source identifier - expandedRow.identifier = result.uuid ?? crypto.randomUUID(); - - // Override values - - expandedRow.label = result.label ?? null; - expandedRow.answer = result.answer ?? null; - expandedRow.explanation = result.explanation ?? null; - - // Extract label-based validation, if present - if ( - row.validation_result && - typeof row.validation_result === "string" - ) { - expandedRow.validation_result = await extractLabelValidation( - expandedRow, - row.validation_result - ); - } - - // Handle metadata - const metadata = result.metadata ?? {}; - expandedRow.metadata = maybeSerializeValue(metadata); - - // Determine value_type - const valueType = result.type ?? inferType(result.value); - expandedRow.value_type = valueType; - - // Cast the value based on its type - const value = maybeSerializeValue(result.value); - expandedRow.value = value; - - // Split into message_references and event_references - const references = result.references ?? []; - const messageRefs = references.filter((ref) => ref.type === "message"); - const eventRefs = references.filter((ref) => ref.type === "event"); - expandedRow.message_references = maybeSerializeValue(messageRefs); - expandedRow.event_references = maybeSerializeValue(eventRefs); - - // don't clear out scan execution fields to avoid incorrect aggregation - // (these represent the scan execution, not individual results) - // (since these aren't for computation, we're keeping them for display) - // expandedRow.scan_total_tokens = null; - // expandedRow.scan_model_usage = null; - - explodedResultsetRows.push(expandedRow); - } - } catch (error) { - console.error("Failed to parse resultset value:", error); - continue; - } - } - - // Create synthetic rows for missing labels with negative expected values - const syntheticRows = await createSyntheticRows( - explodedResultsetRows, - resultObjs - ); - - // Combine with non-resultset rows - if (explodedResultsetRows.length === 0) { - return otherRows; - } else { - // Create an array merging all the rows and convert back to a column table - const otherRowsArray = otherRows.objects() as Record[]; - - const allRowsArray = [ - ...otherRowsArray, - ...explodedResultsetRows, - ...syntheticRows, - ]; - - // Create new table from combined array - return from(allRowsArray); - } -} - -async function extractLabelValidation( - row: Record, - validationResultStr: string -): Promise { - if (!row.label || typeof row.label !== "string") { - return validationResultStr; - } - - try { - const parsedValidation = await asyncJsonParse(validationResultStr); - - // Check if this is label-based validation (dict of label -> bool) - if ( - typeof parsedValidation === "object" && - parsedValidation !== null && - !Array.isArray(parsedValidation) - ) { - // Extract the validation result for this specific label - const validationDict = parsedValidation as Record; - const labelValidation = validationDict[row.label]; - return labelValidation ?? null; - } - - // Not label-based, return as-is - return parsedValidation; - } catch (error) { - // If parsing fails, return original string - return validationResultStr; - } -} - -/** - * Create synthetic rows for missing labels with negative expected values. - * - * When validation_target contains expected labels that are not present in the - * expanded results, and the expected value is "negative" (false, null, etc.), - * this creates synthetic rows for those missing labels. - * - * @param expandedRows - The expanded result rows - * @param resultsetRows - The original resultset rows (used as template) - * @returns Array of synthetic rows to add - */ -async function createSyntheticRows( - expandedRows: Record[], - resultsetRows: Record[] -): Promise[]> { - if (resultsetRows.length === 0 || expandedRows.length === 0) { - return []; - } - - // Check if we have validation_target in the first row - const firstRow = expandedRows[0]; - if ( - !firstRow || - !firstRow.validation_target || - typeof firstRow.validation_target !== "string" - ) { - return []; - } - - try { - // Parse validation_target to check if it's label-based (a dict) - const parsedTarget = await asyncJsonParse( - firstRow.validation_target - ); - if ( - typeof parsedTarget !== "object" || - parsedTarget === null || - Array.isArray(parsedTarget) - ) { - return []; - } - - const validationTarget = parsedTarget as Record; - - // Parse validation_result - const parsedResult = firstRow.validation_result - ? await asyncJsonParse( - typeof firstRow.validation_result === "string" - ? firstRow.validation_result - : JSON.stringify(firstRow.validation_result) - ) - : {}; - const validationResults = - typeof parsedResult === "object" && !Array.isArray(parsedResult) - ? (parsedResult as Record) - : {}; - - // Get all labels present in expanded rows - const presentLabels = new Set( - expandedRows - .map((row) => row.label) - .filter((label) => label !== null && label !== undefined) - ); - - // Get expected labels from validation_target - const expectedLabels = Object.keys(validationTarget); - - // Missing labels = expected but not present - const missingLabels = expectedLabels.filter( - (label) => !presentLabels.has(label) - ); - - // Create synthetic rows for missing labels with negative expected values - const syntheticRows: Record[] = []; - const negativeValues = [false, null, "NONE", "none", 0, ""]; - - for (const label of missingLabels) { - const expectedValue = validationTarget[label]; - - // Only create synthetic row if expected value is negative - if (!negativeValues.includes(expectedValue as never)) { - continue; - } - - // Get a template row from the first resultset row - const templateRow = { ...resultsetRows[0] }; - - // Set result-specific fields for the synthetic row - templateRow.label = label; - templateRow.value = expectedValue; - templateRow.value_type = - typeof expectedValue === "boolean" ? "boolean" : "null"; - templateRow.answer = null; - templateRow.explanation = null; - templateRow.metadata = maybeSerializeValue({}); - templateRow.message_references = maybeSerializeValue([]); - templateRow.event_references = maybeSerializeValue([]); - templateRow.uuid = null; - templateRow.identifier = crypto.randomUUID(); - - // Set validation result for this synthetic row - templateRow.validation_result = validationResults[label] ?? null; - - // NULL out error fields - templateRow.scan_error = null; - templateRow.scan_error_traceback = null; - templateRow.scan_error_type = null; - - // NULL out scan execution fields - templateRow.scan_total_tokens = null; - templateRow.scan_model_usage = null; - - syntheticRows.push(templateRow); - } - - return syntheticRows; - } catch (error) { - // If parsing fails, no synthetic rows - return []; - } -} - -function inferType(value: unknown): ValueType { - if (typeof value === "boolean") { - return "boolean"; - } else if (typeof value === "number") { - return "number"; - } else if (typeof value === "string") { - return "string"; - } else if (Array.isArray(value)) { - return "array"; - } else if (value !== null && typeof value === "object") { - return "object"; - } - return "null"; -} - -const maybeSerializeValue = ( - value: unknown -): string | number | boolean | null => { - if (value === undefined || value === null) { - return null; - } - if ( - typeof value === "string" || - typeof value === "number" || - typeof value === "boolean" - ) { - return value; - } - // Convert complex types (arrays, objects) to JSON strings - return JSON5.stringify(value); -}; diff --git a/src/inspect_scout/_view/www/src/app/utils/arrowHelpers.test.ts b/src/inspect_scout/_view/www/src/app/utils/arrowHelpers.test.ts deleted file mode 100644 index 30920e497..000000000 --- a/src/inspect_scout/_view/www/src/app/utils/arrowHelpers.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { from } from "arquero"; -import { describe, expect, it } from "vitest"; - -import { ScanResultData, ScanResultSummary } from "../types"; - -import { parseScanResultData, parseScanResultSummaries } from "./arrowHelpers"; - -// Typical row data as it would come from arquero .objects() -const typicalSummaryRow = { - identifier: "test-uuid-123", - uuid: "test-uuid-123", - label: "test-label", - explanation: "Test explanation", - input_type: "transcript", - value_type: "object", - value: '{"score": 0.95}', - validation_result: "true", - validation_target: "true", - event_references: '[{"type":"event","id":"evt-1"}]', - message_references: "[]", - transcript_metadata: - '{"model":"gpt-4","task_name":"test-task","id":"task-1","epoch":1}', - transcript_source_id: "source-123", - transcript_task_set: undefined, - transcript_task_id: undefined, - transcript_task_repeat: undefined, - transcript_model: undefined, - scan_error: undefined, - scan_error_refusal: false, - timestamp: "2024-01-15T10:30:00Z", -}; - -const expectedSummary: Partial = { - identifier: "test-uuid-123", - uuid: "test-uuid-123", - label: "test-label", - explanation: "Test explanation", - inputType: "transcript", - valueType: "object", - value: { score: 0.95 }, - validationResult: true, - validationTarget: true, - eventReferences: [{ type: "event", id: "evt-1" }], - messageReferences: [], - transcriptSourceId: "source-123", - // These should be resolved from metadata since they're undefined in row - transcriptModel: "gpt-4", - transcriptTaskSet: "test-task", - transcriptTaskId: "task-1", - transcriptTaskRepeat: 1, - scanErrorRefusal: false, -}; - -// Typical column data as it would come from a ColumnTable -const typicalColumnData: Record = { - identifier: "data-identifier-456", - uuid: "data-uuid-456", - input_type: "message", - value_type: "number", - value: 42, - answer: "test answer", - validation_result: '{"passed":true}', - validation_target: '{"passed":true}', - event_references: "[]", - message_references: '[{"type":"message","id":"msg-1"}]', - input_ids: '["id-1","id-2"]', - metadata: '{"key":"value"}', - scan_events: "[]", - scan_metadata: "{}", - scan_model_usage: "{}", - scan_tags: '["tag1"]', - scanner_params: "{}", - transcript_metadata: '{"model":"claude-3"}', - transcript_source_id: "src-456", - transcript_source_uri: "s3://bucket/path", - transcript_id: "trans-123", - scan_id: "scan-789", - scan_total_tokens: 1500, - scanner_file: "scanner.py", - scanner_key: "test_scanner", - scanner_name: "Test Scanner", - explanation: "Data explanation", - scan_error: null, - scan_error_traceback: null, - scan_error_refusal: false, - timestamp: "2024-02-20T14:00:00Z", -}; - -const expectedData: Partial = { - identifier: "data-identifier-456", - uuid: "data-uuid-456", - inputType: "message", - valueType: "number", - value: 42, - validationResult: { passed: true }, - validationTarget: { passed: true }, - eventReferences: [], - messageReferences: [{ type: "message", id: "msg-1" }], - inputIds: ["id-1", "id-2"], - metadata: { key: "value" }, - scanEvents: [], - scanMetadata: {}, - scanModelUsage: {}, - scanTags: ["tag1"], - scannerParams: {}, - transcriptSourceId: "src-456", - transcriptSourceUri: "s3://bucket/path", - transcriptId: "trans-123", - scanId: "scan-789", - scanTotalTokens: 1500, - scannerFile: "scanner.py", - scannerKey: "test_scanner", - scannerName: "Test Scanner", - explanation: "Data explanation", - scanErrorRefusal: false, - transcriptModel: "claude-3", -}; - -describe("parseScanResultSummaries", () => { - it.each<[object[], Partial[], string]>([ - [[], [], "empty array"], - [[typicalSummaryRow], [expectedSummary], "typical row"], - ])( - "returns expected output for %s", - async (input, expected, _desc: string) => { - const result = await parseScanResultSummaries(input); - expect(result).toHaveLength(expected.length); - for (const [i, r] of result.entries()) { - expect(r).toMatchObject(expected[i]!); - } - } - ); -}); - -describe("parseScanResultData", () => { - it.each<[Record, Partial, string]>([ - [typicalColumnData, expectedData, "typical data"], - ])( - "returns expected output for %s", - async (input, expected, _desc: string) => { - const table = from([input]); - const result = await parseScanResultData(table); - expect(result).toMatchObject(expected); - } - ); -}); diff --git a/src/inspect_scout/_view/www/src/app/utils/arrowHelpers.ts b/src/inspect_scout/_view/www/src/app/utils/arrowHelpers.ts deleted file mode 100644 index 6ba994233..000000000 --- a/src/inspect_scout/_view/www/src/app/utils/arrowHelpers.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { ColumnTable } from "arquero"; - -import { Event, JsonValue, ModelUsage } from "../../types/api-types"; -import { isJson } from "../../utils/json"; -import { asyncJsonParse } from "../../utils/json-worker"; -import { - ScanResultData, - ScanResultReference, - ScanResultSummary, -} from "../types"; - -export const parseScanResultData = async ( - filtered: ColumnTable -): Promise => { - const valueType = filtered.get("value_type", 0) as ValueType; - - const transcript_agent_args_raw = getOptionalColumn( - filtered, - "transcript_agent_args", - 0 - ); - const transcript_score_raw = getOptionalColumn( - filtered, - "transcript_score", - 0 - ); - - // Note that validation_result and validation_target will always a JSON deserializable string as of Jan 7 2026, but prior to this it could be stored as a boolean directly. This conditionality deals with that. - const [ - eventReferences, - inputIds, - messageReferences, - metadata, - scanEvents, - scanMetadata, - scanModelUsage, - scanTags, - scannerParams, - transcriptMetadata, - validationResult, - validationTarget, - value, - transcriptAgentArgs, - transcriptScore, - ] = await Promise.all([ - parseJson(filtered.get("event_references", 0) as string), - parseJson(filtered.get("input_ids", 0) as string), - parseJson(filtered.get("message_references", 0) as string), - parseJson(filtered.get("metadata", 0) as string), - parseJson(filtered.get("scan_events", 0) as string), - parseJson(filtered.get("scan_metadata", 0) as string), - parseJson(filtered.get("scan_model_usage", 0) as string), - parseJson(filtered.get("scan_tags", 0) as string), - parseJson(filtered.get("scanner_params", 0) as string), - parseJson(filtered.get("transcript_metadata", 0) as string), - tryParseJson>( - filtered.get("validation_result", 0) - ), - tryParseJson(filtered.get("validation_target", 0)), - parseSimpleValue(filtered.get("value", 0), valueType), - transcript_agent_args_raw - ? parseJson(transcript_agent_args_raw) - : Promise.resolve(undefined), - transcript_score_raw !== null && transcript_score_raw !== undefined - ? parseJsonValue(transcript_score_raw) - : Promise.resolve(undefined), - ]); - - const identifier = filtered.get("identifier", 0) as string; - const uuid = filtered.get("uuid", 0) as string | undefined; - const timestamp = getOptionalColumn(filtered, "timestamp"); - const answer = filtered.get("answer", 0) as string | undefined; - const label = getOptionalColumn(filtered, "label"); - const explanation = filtered.get("explanation", 0) as string | undefined; - const inputType = filtered.get("input_type", 0) as - | "transcript" - | "message" - | "messages" - | "event" - | "events"; - const scanError = filtered.get("scan_error", 0) as string | undefined; - const scanErrorTraceback = filtered.get("scan_error_traceback", 0) as - | string - | undefined; - const scanErrorRefusal = - getOptionalColumn(filtered, "scan_error_refusal") ?? false; - const scanId = filtered.get("scan_id", 0) as string; - const scanTotalTokens = filtered.get("scan_total_tokens", 0) as number; - const scannerFile = filtered.get("scanner_file", 0) as string; - const scannerKey = filtered.get("scanner_key", 0) as string; - const scannerName = filtered.get("scanner_name", 0) as string; - const transcriptId = filtered.get("transcript_id", 0) as string; - const transcriptSourceId = filtered.get("transcript_source_id", 0) as string; - const transcriptSourceUri = filtered.get( - "transcript_source_uri", - 0 - ) as string; - - const transcriptTaskSet = getOptionalColumn( - filtered, - "transcript_task_set" - ); - const transcriptTaskId = getOptionalColumn( - filtered, - "transcript_task_id" - ); - const transcriptTaskRepeat = getOptionalColumn( - filtered, - "transcript_task_repeat" - ); - const transcriptDate = getOptionalColumn(filtered, "transcript_date"); - const transcriptAgent = getOptionalColumn( - filtered, - "transcript_agent" - ); - const transcriptModel = getOptionalColumn( - filtered, - "transcript_model" - ); - const transcriptSuccess = getOptionalColumn( - filtered, - "transcript_success" - ); - const transcriptTotalTime = getOptionalColumn( - filtered, - "transcript_total_time" - ); - const transcriptTotalTokens = getOptionalColumn( - filtered, - "transcript_total_tokens" - ); - const transcriptMessageCount = getOptionalColumn( - filtered, - "transcript_message_count" - ); - const transcriptError = getOptionalColumn( - filtered, - "transcript_error" - ); - const transcriptLimit = getOptionalColumn( - filtered, - "transcript_limit" - ); - - const baseData = { - identifier, - uuid, - timestamp, - answer, - label, - eventReferences: eventReferences as ScanResultReference[], - explanation, - inputIds: inputIds as string[], - messageReferences: messageReferences as ScanResultReference[], - metadata: metadata as Record, - scanError, - scanErrorTraceback, - scanErrorRefusal, - scanEvents: scanEvents as Event[], - scanId, - scanMetadata: scanMetadata as Record, - scanModelUsage: scanModelUsage as Record, - scanTags: scanTags as string[], - scanTotalTokens, - scannerFile, - scannerKey, - scannerName, - scannerParams: scannerParams as Record, - transcriptId, - transcriptMetadata: transcriptMetadata as Record, - transcriptSourceId, - transcriptSourceUri, - transcriptTaskSet, - transcriptTaskId, - transcriptTaskRepeat, - transcriptAgent, - transcriptAgentArgs: transcriptAgentArgs as Record, - transcriptDate, - transcriptModel, - transcriptScore, - transcriptSuccess, - transcriptTotalTime, - transcriptTotalTokens, - transcriptMessageCount, - transcriptError, - transcriptLimit, - validationResult, - validationTarget, - value: value ?? null, - valueType, - }; - - // Resolve old values from the metadata if not present directly - // this should only be hit if the scan was old enough to not have - // these fields - resolveTranscriptPropertiesFromMetadata(baseData); - - return { ...baseData, inputType }; -}; - -export const parseScanResultSummaries = async ( - rowData: object[] -): Promise => - Promise.all( - rowData.map(async (row) => { - const r = row as Record; - - const valueType = r.value_type as ValueType; - - // Note that validation_result and validation_target will always a JSON deserializable string as of Jan 7 2026, but prior to this it could be stored as a boolean directly. This conditionality deals with that. - const [ - validationResult, - validationTarget, - transcriptMetadata, - eventReferences, - messageReferences, - value, - ] = await Promise.all([ - tryParseJson>(r.validation_result), - tryParseJson(r.validation_target), - parseJson>(r.transcript_metadata as string), - parseJson(r.event_references as string), - parseJson(r.message_references as string), - parseSimpleValue(r.value, valueType), - ]); - - const baseSummary = { - identifier: r.identifier as string, - uuid: r.uuid as string | undefined, - label: r.label as string | undefined, - explanation: r.explanation as string, - eventReferences: eventReferences as ScanResultReference[], - messageReferences: messageReferences as ScanResultReference[], - validationResult: validationResult, - validationTarget: validationTarget, - value: value ?? null, - valueType, - transcriptTaskSet: r.transcript_task_set as string | undefined, - transcriptTaskId: r.transcript_task_id as string | number | undefined, - transcriptTaskRepeat: r.transcript_task_repeat as number | undefined, - transcriptModel: r.transcript_model as string | undefined, - transcriptMetadata: transcriptMetadata || {}, - transcriptSourceId: r.transcript_source_id as string, - scanError: r.scan_error as string, - scanErrorRefusal: r.scan_error_refusal as boolean, - timestamp: r.timestamp ? (r.timestamp as string) : undefined, - }; - - resolveTranscriptPropertiesFromMetadata(baseSummary); - - const inputType = r.input_type as - | "transcript" - | "message" - | "messages" - | "event" - | "events"; - - return { ...baseSummary, inputType }; - }) - ); - -function resolveTranscriptPropertiesFromMetadata< - T extends { - transcriptModel?: string; - transcriptTaskSet?: string; - transcriptTaskId?: string | number; - transcriptTaskRepeat?: number; - transcriptMetadata: Record; - }, ->(data: T): void { - if (data.transcriptModel === undefined) { - data.transcriptModel = data.transcriptMetadata["model"] as string; - } - - if (data.transcriptTaskSet === undefined) { - data.transcriptTaskSet = data.transcriptMetadata["task_name"] as string; - } - - if (data.transcriptTaskId === undefined) { - data.transcriptTaskId = data.transcriptMetadata["id"] as string | number; - } - - if (data.transcriptTaskRepeat === undefined) { - data.transcriptTaskRepeat = data.transcriptMetadata["epoch"] as number; - } -} - -const parseJson = async (text: string | null): Promise => - text !== null ? asyncJsonParse(text) : undefined; - -const tryParseJson = async (text: unknown): Promise => { - try { - return await asyncJsonParse(text as string); - } catch { - return text as T; - } -}; - -type ValueType = "string" | "number" | "boolean" | "null" | "array" | "object"; - -const parseSimpleValue = ( - val: unknown, - valueType: ValueType -): Promise< - string | number | boolean | null | unknown[] | object | undefined -> => - valueType === "object" || valueType === "array" - ? parseJson(val as string) - : Promise.resolve(val as string | number | boolean | null); - -const parseJsonValue = (val?: unknown): Promise => { - if (!val) { - return Promise.resolve(undefined); - } - - if (typeof val === "string" && isJson(val)) { - return parseJson(val).then((parsed) => parsed as JsonValue); - } else { - return Promise.resolve(val as JsonValue); - } -}; - -function getOptionalColumn( - table: ColumnTable, - columnName: string, - rowIndex: number = 0 -): T | undefined { - return table.columnNames().includes(columnName) - ? (table.get(columnName, rowIndex) as T) - : undefined; -} diff --git a/src/inspect_scout/_view/www/src/app/utils/messages.ts b/src/inspect_scout/_view/www/src/app/utils/messages.ts deleted file mode 100644 index 50a878e9d..000000000 --- a/src/inspect_scout/_view/www/src/app/utils/messages.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { - ChatMessage, - ChatMessageSystem, - ChatMessageUser, - ChatMessageAssistant, - ChatMessageTool, - ContentText, - ContentReasoning, - ContentImage, - ContentAudio, - ContentVideo, - ContentData, - ContentToolUse, - ContentDocument, -} from "../../types/api-types"; - -export interface MessagesToStrOptions { - excludeSystem?: boolean; - excludeToolUsage?: boolean; - excludeReasoning?: boolean; -} - -export const messagesToStr = ( - messages: ChatMessage[], - options?: MessagesToStrOptions -): string => { - const opts = options || {}; - return messages - .map((msg) => messageToStr(msg, opts)) - .filter((str): str is string => str !== null) - .join("\n"); -}; - -const messageToStr = ( - message: - | ChatMessageSystem - | ChatMessageUser - | ChatMessageAssistant - | ChatMessageTool, - options: MessagesToStrOptions -): string | null => { - // Exclude system messages if requested - if (options.excludeSystem && message.role === "system") { - return null; - } - - const content = betterContentText( - message.content, - options.excludeToolUsage || false, - options.excludeReasoning || false - ); - - // Handle assistant messages with tool calls - if ( - !options.excludeToolUsage && - message.role === "assistant" && - message.tool_calls - ) { - const assistantMsg = message; - let entry = `${message.role.toUpperCase()}:\n${content}\n`; - - if (assistantMsg.tool_calls) { - for (const tool of assistantMsg.tool_calls) { - const funcName = tool.function; - const args = tool.arguments; - - if (typeof args === "object" && args !== null) { - const argsText = Object.entries(args) - .map(([k, v]) => `${k}: ${String(v)}`) - .join("\n"); - entry += `\nTool Call: ${funcName}\nArguments:\n${argsText}\n`; - } else { - entry += `\nTool Call: ${funcName}\n`; - } - } - } - - return entry; - } - - // Handle tool messages - if (message.role === "tool") { - if (options.excludeToolUsage) { - return null; - } - const toolMsg = message; - const funcName = toolMsg.function || "unknown"; - const errorPart = toolMsg.error - ? `\n\nError in tool call '${funcName}':\n${toolMsg.error.message}\n` - : ""; - return `${message.role.toUpperCase()}:\n${content}${errorPart}\n`; - } - - // Default formatting for system, user, and assistant messages without tool calls - return `${message.role.toUpperCase()}:\n${content}\n`; -}; - -const textFromContent = ( - content: - | ContentText - | ContentReasoning - | ContentImage - | ContentAudio - | ContentVideo - | ContentData - | ContentToolUse - | ContentDocument, - excludeToolUsage: boolean, - excludeReasoning: boolean -): string | null => { - switch (content.type) { - case "text": - return content.text; - - case "reasoning": { - const reasoningContent = content; - if (excludeReasoning) { - return null; - } - const reasoning = reasoningContent.redacted - ? reasoningContent.summary - : reasoningContent.reasoning; - if (!reasoning) { - return null; - } - // Bracket it with start/finish since it could be multiple lines long - return `\n${reasoning}`; - } - - case "tool_use": { - if (excludeToolUsage) { - return null; - } - const toolUse = content; - const errorStr = toolUse.error ? ` ${toolUse.error}` : ""; - return `\nTool Use: ${toolUse.name}(${toolUse.arguments}) -> ${toolUse.result}${errorStr}`; - } - - case "image": - case "audio": - case "video": - case "data": - case "document": - return `<${content.type} />`; - - default: - return null; - } -}; - -const betterContentText = ( - content: - | string - | Array< - | ContentText - | ContentReasoning - | ContentImage - | ContentAudio - | ContentVideo - | ContentData - | ContentToolUse - | ContentDocument - >, - excludeToolUsage: boolean, - excludeReasoning: boolean -): string => { - if (typeof content === "string") { - return content; - } - - const allText = content - .map((c) => textFromContent(c, excludeToolUsage, excludeReasoning)) - .filter((text): text is string => text !== null); - - return allText.join("\n"); -}; diff --git a/src/inspect_scout/_view/www/src/app/utils/refs.tsx b/src/inspect_scout/_view/www/src/app/utils/refs.tsx deleted file mode 100644 index 01c453cd5..000000000 --- a/src/inspect_scout/_view/www/src/app/utils/refs.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { ReactNode, useMemo } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { ChatView } from "../../components/chat/ChatView"; -import { MarkdownReference } from "../../components/MarkdownDivWithReferences"; -import { TranscriptView } from "../../components/transcript/TranscriptView"; -import { scanResultRoute } from "../../router/url"; -import { useScanRoute } from "../hooks/useScanRoute"; -import { - ScanResultInputData, - isEventInput, - isEventsInput, - isMessageInput, - isMessagesInput, - isTranscriptInput, - ScanResultSummary, -} from "../types"; - -export type MakeReferenceUrl = ( - ref: string, - type: "message" | "event" -) => string | undefined; - -export const useMarkdownRefs = ( - summary?: ScanResultSummary, - inputData?: ScanResultInputData -) => { - const { scansDir, scanPath } = useScanRoute(); - const [currentSearchParams] = useSearchParams(); - - // Build URL to the scan result with the appropriate query parameters - // TODO: lint react-hooks/preserve-manual-memoization - the lint seems to be a bug in the rule that doesn't account for the ? - // eslint-disable-next-line react-hooks/preserve-manual-memoization - const buildUrl = useMemo(() => { - if (!summary?.identifier) { - return (queryParams: string) => `?${queryParams}`; - } - - return (queryParams: string) => { - if (!scansDir) { - return `?${queryParams}`; - } - // Start with current search params to preserve validation, etc. - const mergedParams = new URLSearchParams(currentSearchParams); - // Add/override with new params - const newParams = new URLSearchParams(queryParams); - for (const [key, value] of newParams) { - mergedParams.set(key, value); - } - return `#${scanResultRoute(scansDir, scanPath, summary.identifier, mergedParams)}`; - }; - // Use .toString() for currentSearchParams since URLSearchParams object reference - // may not change when URL changes, causing stale closures - }, [summary?.identifier, scanPath, scansDir, currentSearchParams]); - - const refs: MarkdownReference[] = summary - ? toMarkdownRefs( - summary, - (refId: string, type: "message" | "event") => { - if (type === "message") { - return buildUrl(`tab=Result&message=${encodeURIComponent(refId)}`); - } else { - return buildUrl(`tab=Result&event=${encodeURIComponent(refId)}`); - } - }, - inputData - ) - : []; - return refs; -}; - -export const toMarkdownRefs = ( - summary: ScanResultSummary, - makeReferenceUrl: MakeReferenceUrl, - inputData?: ScanResultInputData -) => { - const refLookup = referenceTable(inputData); - - const refs: MarkdownReference[] = []; - for (const ref of summary.messageReferences) { - const renderPreview = refLookup[ref.id]; - const refUrl = makeReferenceUrl(ref.id, "message"); - if (ref.cite && (renderPreview || refUrl)) { - refs.push({ - id: ref.id, - cite: ref.cite, - citePreview: renderPreview, - citeUrl: refUrl, - }); - } - } - - for (const ref of summary.eventReferences) { - const renderPreview = refLookup[ref.id]; - const refUrl = makeReferenceUrl(ref.id, "event"); - if (ref.cite && (renderPreview || refUrl)) { - refs.push({ - id: ref.id, - cite: ref.cite, - citePreview: renderPreview, - citeUrl: refUrl, - }); - } - } - return refs; -}; - -const referenceTable = ( - inputData?: ScanResultInputData -): Record ReactNode> => { - if (!inputData) { - return {}; - } - - if (isMessageInput(inputData)) { - if (!inputData.input.id) { - return {}; - } - return { - [inputData.input.id]: () => { - return ( - - ); - }, - }; - } else if (isMessagesInput(inputData)) { - return inputData.input.reduce ReactNode>>( - (acc, msg) => { - if (msg.id) { - acc[msg.id] = () => { - return ( - - ); - }; - } - return acc; - }, - {} - ); - } else if (isEventInput(inputData)) { - if (!inputData.input.uuid) { - return {}; - } - - return { - [inputData.input.uuid]: () => { - return ( - - ); - }, - }; - } else if (isEventsInput(inputData)) { - return inputData.input.reduce ReactNode>>( - (acc, event, index) => { - if (event.uuid) { - acc[event.uuid] = () => { - return ( - - ); - }; - } - return acc; - }, - {} - ); - } else if (isTranscriptInput(inputData)) { - const eventRefs = (inputData.input.events || []).reduce< - Record ReactNode> - >((acc, event) => { - if (event.uuid) { - acc[event.uuid] = () => { - return ; - }; - } - return acc; - }, {}); - - const messageRefs = (inputData.input.messages || []).reduce< - Record ReactNode> - >((acc, msg) => { - if (msg.id) { - acc[msg.id] = () => { - return ( - - ); - }; - } - return acc; - }, {}); - return { ...eventRefs, ...messageRefs }; - } else { - return {}; - } -}; diff --git a/src/inspect_scout/_view/www/src/app/utils/results.ts b/src/inspect_scout/_view/www/src/app/utils/results.ts deleted file mode 100644 index 6558e94e1..000000000 --- a/src/inspect_scout/_view/www/src/app/utils/results.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ScanResultSummary } from "../types"; - -export interface IdentifierInfo { - taskSet?: string; - id: string | number; - secondaryId?: string | number; - epoch?: number; -} - -export const resultIdentifierStr = ( - summary?: ScanResultSummary -): string | undefined => { - const identifier = resultIdentifier(summary); - if (!identifier) { - return undefined; - } - if (identifier.secondaryId || identifier.epoch) { - const id: string[] = []; - if (identifier.taskSet) { - id.push(identifier.taskSet); - } - id.push(String(identifier.id)); - - const result: string[] = [id.join("/")]; - if (identifier.secondaryId) { - result.push(String(identifier.secondaryId)); - } - if (identifier.epoch) { - result.push(`(${String(identifier.epoch)})`); - } - return result.join(" "); - } -}; - -export const resultIdentifier = ( - summary?: ScanResultSummary -): IdentifierInfo => { - if (!summary) { - return { - id: "unknown", - }; - } - if (summary.inputType === "transcript") { - // Look in the metadata for a sample identifier - const sampleIdentifier = getSampleIdentifier(summary); - if (sampleIdentifier) { - return sampleIdentifier; - } - } else if (summary.inputType === "message") { - const sampleIdentifier = getSampleIdentifier(summary); - return { - id: summary.transcriptSourceId, - secondaryId: sampleIdentifier ? sampleIdentifier.id : undefined, - epoch: sampleIdentifier ? sampleIdentifier.epoch : undefined, - }; - } else if (summary.inputType === "event") { - const sampleIdentifier = getSampleIdentifier(summary); - return { - id: summary.transcriptSourceId, - secondaryId: sampleIdentifier ? sampleIdentifier.id : undefined, - epoch: sampleIdentifier ? sampleIdentifier.epoch : undefined, - }; - } - - return { - id: summary.transcriptSourceId, - }; -}; - -const getSampleIdentifier = ( - summary: ScanResultSummary -): IdentifierInfo | undefined => { - const id = summary.transcriptTaskId; - const epoch = summary.transcriptTaskRepeat; - - if (id && epoch) { - const taskSet = summary.transcriptTaskSet; - return { - id, - epoch, - taskSet, - }; - } - return undefined; -}; - -export const resultLog = (summary: ScanResultSummary): string | undefined => { - if (summary.inputType === "transcript") { - return summary.transcriptMetadata["log"] as string; - } - return undefined; -}; diff --git a/src/inspect_scout/_view/www/src/app/utils/router.ts b/src/inspect_scout/_view/www/src/app/utils/router.ts deleted file mode 100644 index d2a41c43a..000000000 --- a/src/inspect_scout/_view/www/src/app/utils/router.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useMemo } from "react"; -import { useParams } from "react-router-dom"; - -import { useStore } from "../../state/store"; -import { decodeBase64Url } from "../../utils/base64url"; - -export const useTranscriptDirParams = (): string | undefined => { - const params = useParams<{ transcriptsDir?: string }>(); - const setUserTranscriptsDir = useStore( - (state) => state.setUserTranscriptsDir - ); - - const decodedTranscriptDir = useMemo(() => { - if (params.transcriptsDir) { - return decodeBase64Url(params.transcriptsDir); - } - return undefined; - }, [params.transcriptsDir]); - - useEffect(() => { - if (decodedTranscriptDir) { - setUserTranscriptsDir(decodedTranscriptDir); - } - }, [decodedTranscriptDir, setUserTranscriptsDir]); - - return decodedTranscriptDir; -}; diff --git a/src/inspect_scout/_view/www/src/app/utils/scan.ts b/src/inspect_scout/_view/www/src/app/utils/scan.ts deleted file mode 100644 index 48810067b..000000000 --- a/src/inspect_scout/_view/www/src/app/utils/scan.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Status } from "../../types/api-types"; -import { toRelativePath } from "../../utils/path"; - -/** - * Gets the display name for a scan. - * - * Uses the relative path from the scans directory when available, - * providing a more informative title that includes the directory structure. - * - * @param scan - The scan status object, or undefined if loading - * @param scansDir - The base scans directory for computing relative paths - * @returns The scan display name, or undefined if scan is undefined - */ -export function getScanDisplayName( - scan: Status | undefined, - scansDir: string | undefined -): string | undefined { - if (!scan) return undefined; - - // Use relative path if we have a scans directory - if (scansDir && scan.location) { - const relativePath = toRelativePath(scan.location, scansDir); - if (relativePath) { - return relativePath; - } - } - - // Fall back to scan name - return scan.spec.scan_name === "job" ? "scan" : scan.spec.scan_name; -} diff --git a/src/inspect_scout/_view/www/src/app/utils/transcript.ts b/src/inspect_scout/_view/www/src/app/utils/transcript.ts deleted file mode 100644 index e173cde88..000000000 --- a/src/inspect_scout/_view/www/src/app/utils/transcript.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Transcript } from "../../types/api-types"; - -interface TaskNameParts { - taskSet?: string | null; - taskId?: string | number | null; - taskRepeat?: number | null; -} - -/** - * Formats task name parts into a display string. - * - * Produces a string like "taskSet/taskId (taskRepeat)" with optional parts: - * - If only taskSet: "taskSet" - * - If only taskId: "taskId" - * - If both: "taskSet/taskId" - * - With repeat: "taskSet/taskId (1)" - * - * @param parts - Object containing taskSet, taskId, and taskRepeat - * @returns Formatted task name string, or undefined if no parts are available - */ -export function formatTaskName(parts: TaskNameParts): string | undefined { - const { taskSet, taskId, taskRepeat } = parts; - - if (!taskSet && !taskId && taskRepeat === undefined) { - return undefined; - } - - const nameParts: string[] = []; - - if (taskSet) { - nameParts.push(taskSet); - } - - if (taskId !== undefined && taskId !== null) { - if (nameParts.length > 0) { - nameParts.push("/"); - } - nameParts.push(String(taskId)); - } - - if (taskRepeat !== undefined && taskRepeat !== null) { - nameParts.push(` (${taskRepeat})`); - } - - return nameParts.length > 0 ? nameParts.join("") : undefined; -} - -/** - * Gets the display name for a transcript. - * - * Uses taskSet/taskId/taskRepeat when available, falls back to task_id. - * - * @param transcript - The transcript object, or undefined if loading - * @returns The transcript display name, or undefined if transcript is undefined - */ -export function getTranscriptDisplayName( - transcript: Transcript | undefined -): string | undefined { - if (!transcript) return undefined; - - // Try to build a meaningful name from task parts - const formattedName = formatTaskName({ - taskSet: transcript.task_set, - taskId: transcript.task_id, - taskRepeat: transcript.task_repeat, - }); - - // Fall back to task_id if no formatted name available - return formattedName ?? transcript.task_id ?? undefined; -} diff --git a/src/inspect_scout/_view/www/src/app/utils/useScansDir.ts b/src/inspect_scout/_view/www/src/app/utils/useScansDir.ts deleted file mode 100644 index caee32391..000000000 --- a/src/inspect_scout/_view/www/src/app/utils/useScansDir.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useStore } from "../../state/store"; -import { useScanRoute } from "../hooks/useScanRoute"; -import { appAliasedPath, useAppConfig } from "../server/useAppConfig"; - -interface UseScansDirResult { - displayScansDir: string; - resolvedScansDir: string; - resolvedScansDirSource: "route" | "user" | "project" | "cli"; - setScansDir: (path: string) => void; -} - -export function useScansDir(useRouteParam = false): UseScansDirResult { - const config = useAppConfig(); - const { scansDir: routeScansDir } = useScanRoute(); - const userScansDir = useStore((state) => state.userScansDir); - const setUserScansDir = useStore((state) => state.setUserScansDir); - - // TODO: || "" is a smell. Fix them - const resolvedPath = - (useRouteParam ? routeScansDir : null) || - userScansDir || - config.scans.dir || - ""; - - const scanDirSource = - useRouteParam && routeScansDir - ? "route" - : userScansDir - ? "user" - : config.scans.source === "cli" - ? "cli" - : "project"; - - const displayPath = appAliasedPath(config, resolvedPath) || ""; - - return { - displayScansDir: displayPath, - resolvedScansDir: resolvedPath, - resolvedScansDirSource: scanDirSource, - setScansDir: setUserScansDir, - }; -} diff --git a/src/inspect_scout/_view/www/src/app/utils/useTranscriptsDir.ts b/src/inspect_scout/_view/www/src/app/utils/useTranscriptsDir.ts deleted file mode 100644 index e42d97b2c..000000000 --- a/src/inspect_scout/_view/www/src/app/utils/useTranscriptsDir.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useStore } from "../../state/store"; -import { appAliasedPath, useAppConfig } from "../server/useAppConfig"; - -import { useTranscriptDirParams } from "./router"; - -interface UseTranscriptsDirResult { - displayTranscriptsDir: string; - resolvedTranscriptsDir: string; - resolvedTranscriptsDirSource: "route" | "user" | "project" | "cli"; - setTranscriptsDir: (path: string) => void; -} - -export function useTranscriptsDir( - useRouteParam = false -): UseTranscriptsDirResult { - const config = useAppConfig(); - const routeTranscriptsDir = useTranscriptDirParams(); - const userTranscriptsDir = useStore((state) => state.userTranscriptsDir); - const setUserTranscriptsDir = useStore( - (state) => state.setUserTranscriptsDir - ); - - // TODO: || "" is a smell. Fix them - const resolvedPath = - (useRouteParam ? routeTranscriptsDir : null) || - userTranscriptsDir || - config.transcripts?.dir || - ""; - - const resolvedSource = - useRouteParam && routeTranscriptsDir - ? "route" - : userTranscriptsDir - ? "user" - : config.transcripts && config.transcripts?.source === "cli" - ? "cli" - : "project"; - - const displayPath = appAliasedPath(config, resolvedPath) || ""; - - return { - displayTranscriptsDir: displayPath, - resolvedTranscriptsDir: resolvedPath, - resolvedTranscriptsDirSource: resolvedSource, - setTranscriptsDir: setUserTranscriptsDir, - }; -} diff --git a/src/inspect_scout/_view/www/src/app/validation/ValidationPanel.module.css b/src/inspect_scout/_view/www/src/app/validation/ValidationPanel.module.css deleted file mode 100644 index 8aef1c0d9..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/ValidationPanel.module.css +++ /dev/null @@ -1,181 +0,0 @@ -.container { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; -} - -.headerRow { - display: flex; - align-items: center; - gap: 16px; - padding: 1rem 1.25rem 0.75rem 1rem; - flex-shrink: 0; -} - -.title { - font-size: 16px; - font-weight: 600; - color: var(--vscode-foreground); - margin: 0; - flex-shrink: 0; -} - -.spacer { - flex: 1; -} - -.headerActions { - display: flex; - align-items: center; - gap: 4px; -} - -/* Filter controls in header */ -.filterControls { - display: flex; - align-items: center; - gap: 12px; -} - -.searchInput { - width: 180px; - font-size: 0.85rem !important; -} - -.searchInput input { - font-size: 0.85rem !important; -} - -.splitFilter { - display: flex; - align-items: center; - gap: 6px; -} - -.filterLabel { - color: var(--vscode-descriptionForeground); - font-size: 0.85rem; -} - -.splitSelect { - width: 80px !important; - min-width: 80px !important; - max-width: 80px !important; -} - -.iconButton { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - padding: 0; - background: transparent; - border: none; - border-radius: 4px; - color: var(--vscode-descriptionForeground); - cursor: pointer; - transition: - background-color 0.1s, - color 0.1s; -} - -.iconButton:hover { - background: var(--vscode-toolbar-hoverBackground); - color: var(--vscode-foreground); -} - -.iconButton:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.modalContent { - display: flex; - flex-direction: column; - gap: 12px; -} - -.modalContent p { - margin: 0; -} - -.warning { - color: var(--vscode-errorForeground); - font-size: 13px; -} - -.renameInput { - width: 100%; -} - -.content { - flex: 1; - display: flex; - flex-direction: column; - gap: 0; - overflow: hidden; - padding: 0; -} - -.casesSection { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; -} - -.loading { - color: var(--vscode-descriptionForeground); - font-style: italic; - padding: 16px 0; -} - -.error { - color: var(--vscode-errorForeground); - padding: 16px 0; -} - -.emptyState { - color: var(--vscode-descriptionForeground); - text-align: center; - padding: 48px 16px; -} - -.casesPlaceholder { - padding: 16px; - background: var(--vscode-editor-background); - border: 1px solid var(--vscode-panel-border); - border-radius: 4px; - color: var(--vscode-descriptionForeground); -} - -/* Summary wrapper for responsive hiding */ -.summaryWrapper { - flex-shrink: 0; -} - -/* Responsive: Hide summary at medium widths */ -@media (max-width: 1150px) { - .summaryWrapper { - display: none; - } -} - -/* Responsive: Wrap header at narrow widths */ -@media (max-width: 900px) { - .headerRow { - flex-wrap: wrap; - } - - .spacer { - display: none; - } - - .filterControls { - width: 100%; - margin-top: 4px; - order: 10; - } -} diff --git a/src/inspect_scout/_view/www/src/app/validation/ValidationPanel.tsx b/src/inspect_scout/_view/www/src/app/validation/ValidationPanel.tsx deleted file mode 100644 index ed3d6920b..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/ValidationPanel.tsx +++ /dev/null @@ -1,430 +0,0 @@ -import { skipToken } from "@tanstack/react-query"; -import { - VscodeButton, - VscodeOption, - VscodeSingleSelect, - VscodeTextfield, -} from "@vscode-elements/react-elements"; -import { - ChangeEvent, - FC, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; - -import { ApplicationIcons } from "../../components/icons"; -import { Modal } from "../../components/Modal"; -import { NonIdealState } from "../../components/NonIdealState"; -import { TextInput } from "../../components/TextInput"; -import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import { useStore } from "../../state/store"; -import { useAppConfig } from "../server/useAppConfig"; -import { - useBulkDeleteValidationCases, - useDeleteValidationSet, - useRenameValidationSet, - useUpdateValidationCase, - useValidationCases, - useValidationSets, -} from "../server/useValidations"; - -import { ValidationCasesList } from "./components/ValidationCasesList"; -import { ValidationSetSelector } from "./components/ValidationSetSelector"; -import { ValidationSummary } from "./components/ValidationSummary"; -import { extractUniqueSplits, getCaseKey, getFilenameFromUri } from "./utils"; -import styles from "./ValidationPanel.module.css"; - -export const ValidationPanel: FC = () => { - useDocumentTitle("Validation"); - - // Config for transcripts directory - const config = useAppConfig(); - const transcriptsDir = config.transcripts?.dir ?? undefined; - - // State management - const selectedUri = useStore((state) => state.selectedValidationSetUri); - const setSelectedUri = useStore((state) => state.setSelectedValidationSetUri); - const clearValidationState = useStore((state) => state.clearValidationState); - const splitFilter = useStore((state) => state.validationSplitFilter); - const setSplitFilter = useStore((state) => state.setValidationSplitFilter); - const searchText = useStore((state) => state.validationSearchText); - const setSearchText = useStore((state) => state.setValidationSearchText); - - // Modal state - const [showDeleteModal, setShowDeleteModal] = useState(false); - const [showRenameModal, setShowRenameModal] = useState(false); - const [newName, setNewName] = useState(""); - - // Data fetching - const { - data: validationSets, - loading: setsLoading, - error: setsError, - } = useValidationSets(); - - const { - data: cases, - loading: casesLoading, - error: casesError, - } = useValidationCases(selectedUri ?? skipToken); - - // Auto-select first validation set when loaded - useEffect(() => { - if (!selectedUri && validationSets && validationSets.length > 0) { - setSelectedUri(validationSets[0]); - } - }, [selectedUri, validationSets, setSelectedUri]); - - // Mutations - const updateMutation = useUpdateValidationCase(selectedUri ?? ""); - const deleteCasesMutation = useBulkDeleteValidationCases(selectedUri ?? ""); - const deleteSetMutation = useDeleteValidationSet(); - const renameSetMutation = useRenameValidationSet(); - - const handleSelectSet = (uri: string | undefined) => { - // Clear selection and filters when changing sets - clearValidationState(); - setSelectedUri(uri); - }; - - const handleBulkSplitChange = useCallback( - (ids: string[], split: string | null) => { - if (!selectedUri || !cases) return; - - // Build case map at execution time to avoid stale closure - const caseMap = new Map(cases.map((c) => [getCaseKey(c.id), c])); - - // Update all cases in parallel - const updateCases = async () => { - const results = await Promise.allSettled( - ids.map((id) => { - const existingCase = caseMap.get(id); - if (!existingCase) return Promise.resolve(); - - return updateMutation.mutateAsync({ - caseId: id, - data: { - ...existingCase, - split: split, - }, - }); - }) - ); - - const failed = results.filter((r) => r.status === "rejected").length; - if (failed > 0) { - console.error( - `Bulk split update: ${failed} of ${ids.length} updates failed` - ); - } - }; - void updateCases(); - }, - [selectedUri, cases, updateMutation] - ); - - const handleBulkDelete = useCallback( - (ids: string[]) => { - if (!selectedUri) return; - deleteCasesMutation.mutate(ids); - }, - [selectedUri, deleteCasesMutation] - ); - - // Handle single case split change - const handleSingleSplitChange = useCallback( - (caseId: string, split: string | null) => { - if (!selectedUri || !cases) return; - - const caseMap = new Map(cases.map((c) => [getCaseKey(c.id), c])); - const existingCase = caseMap.get(caseId); - if (!existingCase) return; - - updateMutation.mutate({ - caseId, - data: { - ...existingCase, - split: split, - }, - }); - }, - [selectedUri, cases, updateMutation] - ); - - // Handle single case delete - const handleSingleDelete = useCallback( - (caseId: string) => { - if (!selectedUri) return; - deleteCasesMutation.mutate([caseId]); - }, - [selectedUri, deleteCasesMutation] - ); - - // Delete validation set - const handleDeleteSet = () => { - if (!selectedUri) return; - deleteSetMutation.mutate(selectedUri, { - onSuccess: () => { - setShowDeleteModal(false); - clearValidationState(); - setSelectedUri(undefined); - }, - }); - }; - - // Rename validation set - const handleOpenRename = () => { - if (selectedUri) { - // Get current filename without extension for editing - const currentName = getFilenameFromUri(selectedUri, true); - setNewName(currentName); - setShowRenameModal(true); - } - }; - - const handleRenameSet = () => { - if (!selectedUri || !newName.trim()) return; - renameSetMutation.mutate( - { uri: selectedUri, newName: newName.trim() }, - { - onSuccess: (newUri) => { - setShowRenameModal(false); - setNewName(""); - // Select the renamed set - setSelectedUri(newUri); - }, - } - ); - }; - - const handleNameInput = (e: Event) => { - const value = (e.target as HTMLInputElement).value; - setNewName(value); - }; - - const currentFilename = selectedUri ? getFilenameFromUri(selectedUri) : ""; - - // Extract unique splits for filter dropdown - const splits = useMemo(() => extractUniqueSplits(cases ?? []), [cases]); - - // Show nothing while loading to prevent flash of main UI - if (setsLoading) { - return
; - } - - // Check for empty state (no validation sets available) - const hasNoValidationSets = !setsError && validationSets?.length === 0; - - if (hasNoValidationSets) { - return ( -
- - Using Scout Validation - - } - /> -
- ); - } - - // Filter handlers - const handleSplitFilterChange = (e: Event) => { - const value = (e.target as HTMLSelectElement).value; - setSplitFilter(value || undefined); - }; - - const handleSearchChange = (e: ChangeEvent) => { - setSearchText(e.target.value || undefined); - }; - - return ( -
- {/* Header row: Title + Set Selector + Actions + Summary + Filter */} -
-

Validation Set:

- - {setsError ? ( -
- Error loading validation sets: {setsError.message} -
- ) : ( - - )} - - {/* Action icons (right after selector) */} - {selectedUri && ( -
- - -
- )} - - {/* Summary (only when cases loaded) */} - {cases && cases.length > 0 && ( -
- -
- )} - - {/* Spacer to push filter to the right */} -
- - {/* Search and split filter (at far right) */} - {selectedUri && cases && ( -
- -
- Split: - - All - {splits.map((split) => ( - - {split} - - ))} - -
-
- )} -
- - {/* Content */} -
- {selectedUri && ( - <> - {casesLoading ? ( -
Loading cases...
- ) : casesError ? ( -
- Error loading cases: {casesError.message} -
- ) : cases ? ( - - ) : null} - - )} - - {!selectedUri && !setsLoading && ( -
- Select a validation set to view its cases. -
- )} -
- - {/* Delete Confirmation Modal */} - setShowDeleteModal(false)} - onSubmit={deleteSetMutation.isPending ? undefined : handleDeleteSet} - title="Move to Trash" - footer={ - <> - setShowDeleteModal(false)}> - Cancel - - - {deleteSetMutation.isPending ? "Moving..." : "Move to Trash"} - - - } - > -
-

- Are you sure you want to move {currentFilename} to - the trash? -

-
-
- - {/* Rename Modal */} - setShowRenameModal(false)} - onSubmit={ - renameSetMutation.isPending || !newName.trim() - ? undefined - : handleRenameSet - } - title="Rename Validation Set" - footer={ - <> - setShowRenameModal(false)}> - Cancel - - - {renameSetMutation.isPending ? "Renaming..." : "Rename"} - - - } - > -
-

Enter a new name for the validation set:

- -
-
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/CopyMoveCasesModal.module.css b/src/inspect_scout/_view/www/src/app/validation/components/CopyMoveCasesModal.module.css deleted file mode 100644 index 881a912aa..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/CopyMoveCasesModal.module.css +++ /dev/null @@ -1,41 +0,0 @@ -.modalContent { - display: flex; - flex-direction: column; - gap: 16px; -} - -.modalContent p { - margin: 0; - color: var(--vscode-foreground); -} - -.fieldGroup { - display: flex; - flex-direction: column; - gap: 6px; -} - -.label { - font-size: 0.85rem; - color: var(--vscode-descriptionForeground); -} - -.select { - width: 100%; -} - -.textInput { - width: 100%; -} - -.hint { - font-size: 0.8rem; - color: var(--vscode-descriptionForeground); - font-style: italic; -} - -.error { - color: var(--vscode-errorForeground); - font-size: 0.9em; - margin: 0; -} diff --git a/src/inspect_scout/_view/www/src/app/validation/components/CopyMoveCasesModal.tsx b/src/inspect_scout/_view/www/src/app/validation/components/CopyMoveCasesModal.tsx deleted file mode 100644 index 70c5478bb..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/CopyMoveCasesModal.tsx +++ /dev/null @@ -1,382 +0,0 @@ -import { skipToken, useQueryClient } from "@tanstack/react-query"; -import { - VscodeButton, - VscodeOption, - VscodeSingleSelect, - VscodeTextfield, -} from "@vscode-elements/react-elements"; -import { FC, useCallback, useMemo, useState } from "react"; - -import { Modal } from "../../../components/Modal"; -import { useApi } from "../../../state/store"; -import { ValidationCase } from "../../../types/api-types"; -import { - useBulkDeleteValidationCases, - useCreateValidationSet, - useValidationCases, - useValidationSets, - validationQueryKeys, -} from "../../server/useValidations"; -import { - extractUniqueSplits, - generateNewSetUri, - getCaseKey, - getFilenameFromUri, - isValidFilename, -} from "../utils"; - -import styles from "./CopyMoveCasesModal.module.css"; - -/** Sentinel value meaning "keep each case's original split" */ -const KEEP_ORIGINAL_SPLIT = "__keep__"; - -/** Sentinel value meaning "remove split (set to null)" */ -const NO_SPLIT = "__none__"; - -interface CopyMoveCasesModalProps { - /** Whether to show the modal */ - show: boolean; - /** Copy or move mode */ - mode: "copy" | "move"; - /** Source validation set URI */ - sourceUri: string; - /** Selected case IDs */ - selectedIds: string[]; - /** Full case data for selected cases */ - selectedCases: ValidationCase[]; - /** Callback when modal is closed */ - onHide: () => void; - /** Callback on successful operation (used to clear selection) */ - onSuccess: () => void; -} - -/** - * Modal for copying or moving validation cases to another validation set. - * Supports selecting an existing set or creating a new one. - */ -export const CopyMoveCasesModal: FC = ({ - show, - mode, - sourceUri, - selectedIds, - selectedCases, - onHide, - onSuccess, -}) => { - const api = useApi(); - const queryClient = useQueryClient(); - - // Target selection state - const [targetUri, setTargetUri] = useState(undefined); - // Default to keeping original splits - const [targetSplit, setTargetSplit] = useState(KEEP_ORIGINAL_SPLIT); - - // New set creation state - const [showNewSetInput, setShowNewSetInput] = useState(false); - const [newSetName, setNewSetName] = useState(""); - - // Operation state - const [isProcessing, setIsProcessing] = useState(false); - const [error, setError] = useState(null); - - // Fetch all validation sets - const { data: validationSets } = useValidationSets(); - - // Filter out the source set from available targets - const availableTargets = useMemo(() => { - return (validationSets ?? []).filter((uri) => uri !== sourceUri); - }, [validationSets, sourceUri]); - - // Fetch target set's cases to extract splits - const { data: targetCases } = useValidationCases(targetUri ?? skipToken); - - // Extract splits from target set - const targetSplits = useMemo(() => { - return extractUniqueSplits(targetCases ?? []); - }, [targetCases]); - - // Mutations - const createSetMutation = useCreateValidationSet(); - const deleteCasesMutation = useBulkDeleteValidationCases(sourceUri); - - // Reset state when modal opens/closes - const handleHide = useCallback(() => { - if (isProcessing) { - return; // Don't allow closing during processing - } - setTargetUri(undefined); - setTargetSplit(KEEP_ORIGINAL_SPLIT); - setShowNewSetInput(false); - setNewSetName(""); - setError(null); - onHide(); - }, [onHide, isProcessing]); - - // Handle target set selection - const handleTargetChange = (e: Event) => { - const value = (e.target as HTMLSelectElement).value; - if (value === "__new__") { - setShowNewSetInput(true); - setNewSetName(""); - setTargetUri(undefined); - } else { - setShowNewSetInput(false); - setTargetUri(value || undefined); - } - setTargetSplit(KEEP_ORIGINAL_SPLIT); // Reset split when target changes - setError(null); - }; - - // Handle new set name input - const handleNewSetNameInput = (e: Event) => { - setNewSetName((e.target as HTMLInputElement).value); - setError(null); - }; - - // Handle split selection change - const handleSplitChange = (e: Event) => { - const value = (e.target as HTMLSelectElement).value; - setTargetSplit(value); - }; - - // Create new validation set - const handleCreateNewSet = async (): Promise => { - const trimmedName = newSetName.trim(); - - // Validate filename - const validation = isValidFilename(trimmedName); - if (!validation.isValid) { - setError(validation.error ?? "Invalid filename"); - return undefined; - } - - const newUri = generateNewSetUri(sourceUri, trimmedName); - - // Check if this would be the same as source - if (newUri === sourceUri) { - setError("Cannot copy/move to the same validation set"); - return undefined; - } - - // Check if a set with this name already exists - if (validationSets?.includes(newUri)) { - setError("A validation set with this name already exists"); - return undefined; - } - - try { - await createSetMutation.mutateAsync({ path: newUri, cases: [] }); - return newUri; - } catch (err) { - const message = - err instanceof Error ? err.message : "Failed to create validation set"; - setError(message); - return undefined; - } - }; - - // Get the split value to use for a case - const getSplitForCase = (originalCase: ValidationCase): string | null => { - if (targetSplit === KEEP_ORIGINAL_SPLIT) { - return originalCase.split ?? null; - } - if (targetSplit === NO_SPLIT) { - return null; - } - return targetSplit; - }; - - // Copy cases to target - const copyCasesToTarget = async (destUri: string): Promise => { - const results = await Promise.allSettled( - selectedCases.map((c) => - api.upsertValidationCase(destUri, getCaseKey(c.id), { - id: c.id, - target: c.target, - labels: c.labels, - split: getSplitForCase(c), - predicate: c.predicate ?? undefined, - }) - ) - ); - - const succeeded = results.filter((r) => r.status === "fulfilled").length; - const failed = results.filter((r) => r.status === "rejected").length; - - if (failed > 0 && succeeded === 0) { - setError(`All ${failed} copy operations failed`); - return false; - } - - if (failed > 0) { - // Partial success - show warning but continue - setError( - `Warning: ${failed} of ${selectedIds.length} cases failed to copy. ${succeeded} succeeded.` - ); - // Still return true to proceed with move deletion if applicable - } - - return true; - }; - - // Handle form submission - const handleSubmit = async () => { - setIsProcessing(true); - setError(null); - - try { - // Determine final target URI - let finalTargetUri = targetUri; - - if (showNewSetInput) { - finalTargetUri = await handleCreateNewSet(); - if (!finalTargetUri) { - setIsProcessing(false); - return; - } - } - - if (!finalTargetUri) { - setError("Please select a target validation set"); - setIsProcessing(false); - return; - } - - // Copy cases to target - const copySuccess = await copyCasesToTarget(finalTargetUri); - if (!copySuccess) { - setIsProcessing(false); - return; - } - - // Invalidate target set cache to show new cases - void queryClient.invalidateQueries({ - queryKey: validationQueryKeys.cases(finalTargetUri), - }); - - // If move mode, delete from source - if (mode === "move") { - try { - await deleteCasesMutation.mutateAsync(selectedIds); - } catch (err) { - // Cases were copied but deletion failed - inform user - const message = err instanceof Error ? err.message : "Unknown error"; - setError( - `Cases copied successfully, but failed to delete from source: ${message}` - ); - setIsProcessing(false); - return; - } - } - - // Success! - onSuccess(); - handleHide(); - } catch (err) { - const message = err instanceof Error ? err.message : "Operation failed"; - setError(message); - } finally { - setIsProcessing(false); - } - }; - - const title = mode === "copy" ? "Copy Cases" : "Move Cases"; - const actionLabel = mode === "copy" ? "Copy" : "Move"; - const processingLabel = mode === "copy" ? "Copying..." : "Moving..."; - - const canSubmit = - !isProcessing && (targetUri || (showNewSetInput && newSetName.trim())); - - return ( - void handleSubmit() : undefined} - title={title} - footer={ - <> - - Cancel - - void handleSubmit()} - disabled={!canSubmit} - > - {isProcessing ? processingLabel : actionLabel} - - - } - > -
-

- {actionLabel} {selectedIds.length}{" "} - {selectedIds.length === 1 ? "case" : "cases"} to another validation - set. -

- - {/* Target set selector */} -
- - - Select a validation set... - {availableTargets.map((uri) => ( - - {getFilenameFromUri(uri)} - - ))} - Create new set... - -
- - {/* New set name input */} - {showNewSetInput && ( -
- - - - Will be created as {newSetName.trim() || "name"}.csv - -
- )} - - {/* Split selector for target */} - {(targetUri || (showNewSetInput && newSetName.trim())) && ( -
- - - - Keep original splits - - No split - {targetSplits.map((split) => ( - - {split} - - ))} - -
- )} - - {/* Error message */} - {error &&

{error}

} -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationBulkActions.module.css b/src/inspect_scout/_view/www/src/app/validation/components/ValidationBulkActions.module.css deleted file mode 100644 index 6f1dd2332..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationBulkActions.module.css +++ /dev/null @@ -1,39 +0,0 @@ -.container { - display: flex; - align-items: center; - gap: 12px; - padding: 0.5rem 1rem; - background: var(--vscode-editor-inactiveSelectionBackground); - border-bottom: solid var(--bs-light-border-subtle) 1px; -} - -.selectedCount { - font-weight: 500; - color: var(--vscode-foreground); -} - -.modalContent { - display: flex; - flex-direction: column; - gap: 12px; -} - -.modalContent p { - margin: 0; - color: var(--vscode-foreground); -} - -.splitSelector { - display: flex; - flex-direction: column; - gap: 8px; -} - -.customInput { - margin-top: 4px; -} - -.warning { - color: var(--vscode-errorForeground); - font-size: 0.9em; -} diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationBulkActions.tsx b/src/inspect_scout/_view/www/src/app/validation/components/ValidationBulkActions.tsx deleted file mode 100644 index e01443963..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationBulkActions.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { - VscodeButton, - VscodeOption, - VscodeSingleSelect, - VscodeTextfield, -} from "@vscode-elements/react-elements"; -import { FC, useMemo, useState } from "react"; - -import { Modal } from "../../../components/Modal"; -import { ValidationCase } from "../../../types/api-types"; -import { extractUniqueSplits } from "../utils"; - -import styles from "./ValidationBulkActions.module.css"; - -interface ValidationBulkActionsProps { - cases: ValidationCase[]; - selectedIds: string[]; - onBulkSplitChange: (ids: string[], split: string | null) => void; - onBulkDelete: (ids: string[]) => void; - isUpdating?: boolean; - isDeleting?: boolean; -} - -/** - * Bulk actions for selected validation cases. - * Includes split assignment and delete functionality. - */ -export const ValidationBulkActions: FC = ({ - cases, - selectedIds, - onBulkSplitChange, - onBulkDelete, - isUpdating, - isDeleting, -}) => { - const [showDeleteModal, setShowDeleteModal] = useState(false); - const [showSplitModal, setShowSplitModal] = useState(false); - const [selectedSplit, setSelectedSplit] = useState(""); - const [customSplit, setCustomSplit] = useState(""); - const [useCustomSplit, setUseCustomSplit] = useState(false); - - // Extract unique splits from cases - const existingSplits = useMemo(() => extractUniqueSplits(cases), [cases]); - - const selectedCount = selectedIds.length; - - const handleSplitChange = (e: Event) => { - const value = (e.target as HTMLSelectElement).value; - if (value === "__custom__") { - setUseCustomSplit(true); - setSelectedSplit(""); - } else { - setUseCustomSplit(false); - setSelectedSplit(value); - setCustomSplit(""); - } - }; - - const handleCustomSplitInput = (e: Event) => { - const value = (e.target as HTMLInputElement).value; - setCustomSplit(value); - }; - - const handleAssignSplit = () => { - const split = useCustomSplit ? customSplit : selectedSplit; - onBulkSplitChange(selectedIds, split || null); - setShowSplitModal(false); - setSelectedSplit(""); - setCustomSplit(""); - setUseCustomSplit(false); - }; - - const handleDelete = () => { - onBulkDelete(selectedIds); - setShowDeleteModal(false); - }; - - if (selectedCount === 0) { - return null; - } - - return ( -
- {selectedCount} selected - - setShowSplitModal(true)} - disabled={isUpdating || isDeleting} - > - Assign Split - - - setShowDeleteModal(true)} - disabled={isUpdating || isDeleting} - > - Delete - - - {/* Split Assignment Modal */} - setShowSplitModal(false)} - title="Assign Split" - footer={ - <> - setShowSplitModal(false)}> - Cancel - - - {isUpdating ? "Assigning..." : "Assign"} - - - } - > -
-

- Assign a split to {selectedCount} selected{" "} - {selectedCount === 1 ? "case" : "cases"}. -

- -
- - Remove split - {existingSplits.map((split) => ( - - {split} - - ))} - Custom... - - - {useCustomSplit && ( - - )} -
-
-
- - {/* Delete Confirmation Modal */} - setShowDeleteModal(false)} - title="Confirm Delete" - footer={ - <> - setShowDeleteModal(false)}> - Cancel - - - {isDeleting ? "Deleting..." : "Delete"} - - - } - > -
-

- Are you sure you want to delete {selectedCount}{" "} - {selectedCount === 1 ? "case" : "cases"}? -

-

This action cannot be undone.

-
-
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseCard.module.css b/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseCard.module.css deleted file mode 100644 index ce950dd5e..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseCard.module.css +++ /dev/null @@ -1,223 +0,0 @@ -.card { - display: grid; - /* grid-template-columns set dynamically via inline style */ - align-items: center; - gap: 12px; - padding: calc(0.3rem + 1px) 1rem; - background: transparent; - border: none; - border-radius: 0; - border-bottom: solid 1px var(--bs-border-color); - cursor: pointer; -} - -.card:hover { - background: var( - --vscode-list-inactiveSelectionBackground, - rgba(128, 128, 128, 0.04) - ); -} - -.card.selected { - /* No special background for selected rows */ -} - -.checkbox { - display: flex; - align-items: center; - justify-content: center; - transform: scale(0.85); -} - -.transcriptCell { - display: flex; - flex-direction: column; - min-width: 0; - overflow: hidden; -} - -.idLink { - background: none; - border: none; - padding: 0; - color: #5a9bcf; - cursor: pointer; - font-size: 0.85rem; - text-decoration: none; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-align: left; -} - -.idLink:hover:not(:disabled) { - text-decoration: underline; -} - -.idLink:disabled { - color: var(--vscode-disabledForeground); - cursor: default; -} - -.labelsCell { - text-align: right; - min-width: 80px; - padding-right: 8px; - font-size: 0.8rem; - color: var(--vscode-descriptionForeground); -} - -.targetCell { - text-align: right; - min-width: 80px; - padding-right: 8px; -} - -.target { - color: var(--vscode-descriptionForeground); - font-size: 0.85rem; -} - -/* Boolean target badges */ -.targetTrue { - display: inline-flex; - justify-content: center; - align-items: center; - border-radius: 5px; - padding: 4px 8px; - font-size: 0.7rem; - background-color: var(--bs-success); - border: solid var(--bs-success) 1px; - color: var(--bs-body-bg); - width: 4.5em; -} - -.targetFalse { - display: inline-flex; - justify-content: center; - align-items: center; - border-radius: 5px; - padding: 4px 8px; - font-size: 0.7rem; - background-color: var(--bs-danger); - border: solid var(--bs-danger) 1px; - color: var(--bs-body-bg); - width: 4.5em; -} - -/* Subtle split selector */ -.splitSelect { - width: 90px !important; - min-width: 90px !important; - max-width: 90px !important; - --vscode-dropdown-border: transparent; - --vscode-dropdown-background: transparent; -} - -.splitSelect:hover { - --vscode-dropdown-background: var(--vscode-list-hoverBackground); -} - -/* Action buttons */ -.actions { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 4px; - min-width: 56px; -} - -.actionButton { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - padding: 0; - background: none; - border: none; - border-radius: 4px; - color: var(--vscode-descriptionForeground); - cursor: pointer; - opacity: 0.6; - transition: - opacity 0.15s ease, - background-color 0.15s ease; -} - -.actionButton:hover:not(:disabled) { - opacity: 1; - background: var(--vscode-list-hoverBackground); -} - -.actionButton:disabled { - opacity: 0.3; - cursor: default; -} - -/* Transcript details (below ID link) */ -.detailsRow { - color: var(--vscode-descriptionForeground); - font-size: 0.8rem; - line-height: 1.2; - padding-bottom: 1px; -} - -/* Warning for missing transcripts */ -.notFoundRow { - display: flex; - align-items: center; - gap: 6px; - color: var(--vscode-editorWarning-foreground, #cca700); - font-size: 0.8rem; - line-height: 1.2; - padding-bottom: 1px; -} - -.notFoundRow i { - font-size: 0.75rem; -} - -/* Modal styles */ -.modalContent { - display: flex; - flex-direction: column; - gap: 12px; -} - -.modalContent p { - margin: 0; -} - -.warning { - color: var(--vscode-errorForeground); - font-size: 0.9rem; -} - -.modalButton { - padding: 6px 14px; - border: 1px solid var(--vscode-button-border, transparent); - border-radius: 2px; - background: var(--vscode-button-secondaryBackground); - color: var(--vscode-button-secondaryForeground); - cursor: pointer; - font-size: 13px; -} - -.modalButton:hover { - background: var(--vscode-button-secondaryHoverBackground); -} - -.modalButtonPrimary { - background: var(--vscode-button-background); - color: var(--vscode-button-foreground); -} - -.modalButtonPrimary:hover { - background: var(--vscode-button-hoverBackground); -} - -.modalButtonPrimary:disabled { - opacity: 0.5; - cursor: default; -} diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseCard.tsx b/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseCard.tsx deleted file mode 100644 index 9c25809e8..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseCard.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import { VscodeCheckbox } from "@vscode-elements/react-elements"; -import React, { CSSProperties, FC, useState } from "react"; -import { useNavigate } from "react-router-dom"; - -import { ApplicationIcons } from "../../../components/icons"; -import { Modal } from "../../../components/Modal"; -import { transcriptRoute } from "../../../router/url"; -import { TranscriptInfo, ValidationCase } from "../../../types/api-types"; -import { getIdText } from "../utils"; - -import styles from "./ValidationCaseCard.module.css"; -import { ValidationSplitSelector } from "./ValidationSplitSelector"; - -interface ValidationCaseCardProps { - validationCase: ValidationCase; - transcript?: TranscriptInfo; - transcriptsDir: string | undefined; - validationSetUri?: string; - isSelected: boolean; - onSelectionChange: (selected: boolean) => void; - existingSplits: string[]; - onSplitChange?: (split: string | null) => void; - onDelete?: () => void; - isUpdating?: boolean; - isDeleting?: boolean; - showLabels?: boolean; - showTarget?: boolean; - gridStyle?: CSSProperties; -} - -/** - * Format the target value for display (non-boolean values). - */ -const formatTarget = (target: ValidationCase["target"]): string => { - if (typeof target === "string") { - return target; - } - if (typeof target === "number") { - return String(target); - } - if (Array.isArray(target)) { - return target.map(String).join(", "); - } - if (target === null || target === undefined) { - return ""; - } - return JSON.stringify(target); -}; - -/** - * Render the target value with appropriate styling. - * Boolean targets get styled badges, others get plain text. - */ -const renderTarget = ( - target: ValidationCase["target"], - predicate: string | null | undefined -): React.ReactNode => { - const showPredicate = predicate && predicate !== "eq"; - const predicatePrefix = showPredicate ? `(${predicate}) ` : ""; - - if (typeof target === "boolean") { - return ( - <> - {predicatePrefix} - - {String(target)} - - - ); - } - - const targetText = formatTarget(target); - if (!targetText) return null; - - return ( - - {predicatePrefix} - {targetText} - - ); -}; - -/** - * Format the score value for display. - */ -const formatScore = (score: TranscriptInfo["score"]): string => { - if (score === null || score === undefined) { - return "—"; - } - if (typeof score === "number") { - return String(score); - } - if (typeof score === "boolean") { - return score ? "1" : "0"; - } - if (typeof score === "string") { - return score; - } - return JSON.stringify(score); -}; - -/** - * Format labels for display. - * Shows comma-separated label: true/false pairs, with true values first. - */ -const formatLabels = ( - labels: Record | null | undefined -): string => { - if (!labels) return ""; - const entries = Object.entries(labels); - // Sort: true values first, then false - entries.sort(([, a], [, b]) => (b ? 1 : 0) - (a ? 1 : 0)); - return entries.map(([k, v]) => `${k}: ${v}`).join(", "); -}; - -/** - * Build the transcript details line. - * Format: / () - - (score: ) - */ -const buildTranscriptDetails = (transcript: TranscriptInfo): string => { - const parts: string[] = []; - - // Task info: task_set/task_id (repeat) - if (transcript.task_set || transcript.task_id) { - let taskPart = ""; - if (transcript.task_set && transcript.task_id) { - taskPart = `${transcript.task_set}/${transcript.task_id}`; - } else if (transcript.task_set) { - taskPart = transcript.task_set; - } else if (transcript.task_id) { - taskPart = transcript.task_id; - } - if ( - transcript.task_repeat !== null && - transcript.task_repeat !== undefined - ) { - taskPart += ` (${transcript.task_repeat})`; - } - parts.push(taskPart); - } - - // Agent - if (transcript.agent) { - parts.push(transcript.agent); - } - - // Model - if (transcript.model) { - parts.push(transcript.model); - } - - // Score - const scoreStr = formatScore(transcript.score); - parts.push(`score: ${scoreStr}`); - - return parts.join(" - "); -}; - -/** - * Card component for displaying a single validation case. - */ -export const ValidationCaseCard: FC = ({ - validationCase, - transcript, - transcriptsDir, - validationSetUri, - isSelected, - onSelectionChange, - existingSplits, - onSplitChange, - onDelete, - isUpdating, - isDeleting, - showLabels, - showTarget, - gridStyle, -}) => { - const navigate = useNavigate(); - - const { id, target, split, predicate, labels } = validationCase; - - // Modal state - const [showDeleteModal, setShowDeleteModal] = useState(false); - - // Get display text for the ID - const idText = getIdText(id); - - // Render target with appropriate styling - const targetElement = renderTarget(target, predicate); - - // Handle navigation to transcript (only works for single string IDs) - const handleNavigateToTranscript = () => { - const singleId = Array.isArray(id) ? id[0] : id; - if (transcriptsDir && singleId) { - void navigate( - transcriptRoute(transcriptsDir, singleId, undefined, validationSetUri) - ); - } - }; - - const handleCheckboxChange = (e: Event) => { - const checked = (e.target as HTMLInputElement).checked; - onSelectionChange(checked); - }; - - // Handle row click to toggle selection - const handleRowClick = (e: React.MouseEvent) => { - // Don't toggle if clicking on interactive elements - const target = e.target as HTMLElement; - if ( - target.closest("button") || - target.closest("vscode-checkbox") || - target.closest("vscode-single-select") - ) { - return; - } - onSelectionChange(!isSelected); - }; - - const handleDelete = () => { - onDelete?.(); - setShowDeleteModal(false); - }; - - // Format labels for display - const labelsText = formatLabels(labels); - - return ( -
- {/* Checkbox column */} -
- -
- - {/* Transcript column (ID + details) */} -
- - {transcript ? ( -
- {buildTranscriptDetails(transcript)} -
- ) : ( -
- - Not found in project transcripts -
- )} -
- - {/* Labels column (conditional) */} - {showLabels &&
{labelsText}
} - - {/* Target column (conditional) */} - {showTarget &&
{targetElement}
} - - {/* Split column */} - {onSplitChange ? ( - - ) : ( - {split ?? "—"} - )} - - {/* Actions column */} -
- - {onDelete && ( - - )} -
- - {/* Delete Confirmation Modal */} - setShowDeleteModal(false)} - onSubmit={handleDelete} - title="Delete Case" - footer={ - <> - - - - } - > -
-

Are you sure you want to delete this validation case?

-

This action cannot be undone.

-
-
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseEditor.module.css b/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseEditor.module.css deleted file mode 100644 index 5b56a516b..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseEditor.module.css +++ /dev/null @@ -1,156 +0,0 @@ -.container { - display: flex; - flex-direction: column; - padding: 0; -} - -.header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.5rem; - flex-shrink: 0; - height: 39px; - border-bottom: 1px solid var(--bs-border-color); - background-color: var(--bs-light); -} - -.headerTitle { - margin: 0; - font-size: var(--inspect-font-size-smallest); - color: var(--bs-body-color); - text-transform: uppercase; - font-weight: 400; -} - -.headerIcon { - margin-right: 0.5em; -} - -.headerSecondary { - font-size: var(--inspect-font-size-smallest); - color: var(--bs-secondary-color); -} - -.content { - flex: 1; - overflow-y: auto; -} - -.placeholder { - color: var(--bs-secondary-color); - font-size: 0.875rem; - text-align: center; - margin-top: 2rem; -} - -.textValue { - font-family: var(--bs-font-monospace); - font-size: var(--inspect-font-size-smaller); -} - -.panel { - background-color: var(--bs-body-bg); - display: flex; - flex-direction: column; - gap: 0.75rem; - padding: 0.5rem 0.5rem 1.5rem 0.5rem; -} - -/* Make all form controls full width within the panel */ -.panel :global(vscode-single-select), -.panel :global(vscode-textfield), -.panel :global(button) { - width: 100% !important; - min-width: 0 !important; - max-width: 100% !important; -} - -.clickable { - cursor: pointer; -} - -.idLabel { - margin-right: 0.5em; -} - -.saveStatusContainer { - position: fixed; - bottom: 0.1rem; - right: 0.1rem; - padding: 0.25rem 0.5rem; - background-color: var(--bs-body-bg); - border-radius: var(--bs-border-radius); - max-width: 175px; - line-height: 1.3; - pointer-events: none; - transition: opacity 300ms ease-in-out; -} - -.saveStatusContainer.saveStatusHidden { - opacity: 0; -} - -.saveStatusContainer.saveStatusError { - opacity: 1; - color: var(--bs-danger); -} - -.saveStatus { - font-size: var(--inspect-font-size-smallest); - color: var(--bs-secondary-color); - word-wrap: break-word; - overflow-wrap: break-word; - display: block; -} - -.createError { - color: var(--vscode-errorForeground); - font-size: var(--inspect-font-size-smallest); - margin-top: 0.25rem; -} - -.headerActionButton { - display: flex; - align-items: center; - justify-content: center; - width: 12px; - height: 12px; - padding: 5px; - background: none; - border: none; - border-radius: 4px; - color: var(--vscode-descriptionForeground); - cursor: pointer; - opacity: 0.6; - font-size: 12px; - pointer-events: auto; - position: relative; - z-index: 10; -} - -.headerActionButton:hover:not(:disabled) { - opacity: 1; - background: var(--vscode-toolbar-hoverBackground); - color: var(--vscode-errorForeground); -} - -.headerActionButton:disabled { - opacity: 0.3; - cursor: default; -} -.infoBox { - display: grid; - grid-template-columns: auto 1fr; - column-gap: 0.3rem; -} - -.idField { - display: grid; - grid-template-columns: auto 1fr; -} - -.idValue { - overflow-wrap: break-word; - word-break: break-all; -} diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseEditor.tsx b/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseEditor.tsx deleted file mode 100644 index f79f1963a..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseEditor.tsx +++ /dev/null @@ -1,690 +0,0 @@ -import { skipToken, useQueryClient } from "@tanstack/react-query"; -import { - VscodeDivider, - VscodeRadio, - VscodeRadioGroup, -} from "@vscode-elements/react-elements"; -import clsx from "clsx"; -import React, { FC, ReactNode, useCallback, useEffect, useState } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { ConfirmationDialog } from "../../../components/ConfirmationDialog"; -import { ErrorPanel } from "../../../components/ErrorPanel"; -import { ApplicationIcons } from "../../../components/icons"; -import { LoadingBar } from "../../../components/LoadingBar"; -import { MenuActionButton } from "../../../components/MenuActionButton"; -import { - getValidationParam, - getValidationSetParam, - updateValidationParam, - updateValidationSetParam, -} from "../../../router/url"; -import { useStore } from "../../../state/store"; -import { - JsonValue, - ValidationCase, - ValidationCaseRequest, -} from "../../../types/api-types"; -import { Field } from "../../project/components/FormFields"; -import { useAppConfig } from "../../server/useAppConfig"; -import { - useCreateValidationSet, - useDeleteValidationCase, - useUpdateValidationCase, - useValidationCase, - useValidationCases, - useValidationSets, - validationQueryKeys, -} from "../../server/useValidations"; -import { - extractUniqueLabels, - extractUniqueSplits, - hasValidationSetExtension, - isValidFilename, -} from "../utils"; - -import styles from "./ValidationCaseEditor.module.css"; -import { ValidationCaseLabelsEditor } from "./ValidationCaseLabelsEditor"; -import { - extractUniquePredicates, - ValidationCasePredicateSelector, -} from "./ValidationCasePredicateSelector"; -import { ValidationCaseTargetEditor } from "./ValidationCaseTargetEditor"; -import { ValidationSetSelector } from "./ValidationSetSelector"; -import { ValidationSplitSelector } from "./ValidationSplitSelector"; - -interface ValidationCaseEditorProps { - transcriptId: string; - className?: string | string[]; -} - -export const ValidationCaseEditor: FC = ({ - transcriptId, - className, -}) => { - const [searchParams] = useSearchParams(); - const editorValidationSetUri = useStore( - (state) => state.editorSelectedValidationSetUri - ); - const setEditorSelectedValidationSetUri = useStore( - (state) => state.setEditorSelectedValidationSetUri - ); - const { - data: setsData, - loading: setsLoading, - error: setsError, - } = useValidationSets(); - - const { - data: caseData, - loading: caseLoading, - error: caseError, - } = useValidationCase( - !editorValidationSetUri - ? skipToken - : { - url: editorValidationSetUri, - caseId: transcriptId, - } - ); - - const { - data: casesData, - loading: casesLoading, - error: casesError, - } = useValidationCases( - editorValidationSetUri ? editorValidationSetUri : skipToken - ); - - // Initialize from URL param or fall back to first available set - // URL param always takes precedence when present and valid - useEffect(() => { - if (!setsData || setsData.length === 0) return; - - const validationSetParam = getValidationSetParam(searchParams); - if (validationSetParam && setsData.includes(validationSetParam)) { - // URL param is valid - use it (even if store has a different value) - if (editorValidationSetUri !== validationSetParam) { - setEditorSelectedValidationSetUri(validationSetParam); - } - } else if (!editorValidationSetUri) { - // No URL param and no store value - fall back to first set - setEditorSelectedValidationSetUri(setsData[0]); - } - }, [ - setsData, - searchParams, - editorValidationSetUri, - setEditorSelectedValidationSetUri, - ]); - - const error = setsError || casesError || caseError; - const loading = - setsLoading || casesLoading || (!!editorValidationSetUri && caseLoading); - const showPanel = !setsLoading; - - return ( - <> - {error && ( - - )} - {!error && ( - <> - - {showPanel && setsData && ( - - )} - - )} - - ); -}; - -type ValidationType = "target" | "labels"; - -interface ValidationCaseEditorComponentProps { - transcriptId: string; - validationSets: string[]; - editorValidationSetUri?: string; - validationCase?: ValidationCase | null; - validationCases?: ValidationCase[]; - className?: string | string[]; -} - -const ValidationCaseEditorComponent: FC = ({ - transcriptId, - validationSets, - editorValidationSetUri, - validationCase: caseData, - validationCases, - className, -}) => { - const config = useAppConfig(); - const queryClient = useQueryClient(); - const setEditorSelectedValidationSetUri = useStore( - (state) => state.setEditorSelectedValidationSetUri - ); - const [, setSearchParams] = useSearchParams(); - - // Save status state - const [saveStatus, setSaveStatus] = useState< - "idle" | "saving" | "saved" | "error" - >("idle"); - const [saveError, setSaveError] = useState(null); - - // Create set status state - const [createError, setCreateError] = useState(null); - const createSetMutation = useCreateValidationSet(); - - // Delete case state - const [showDeleteModal, setShowDeleteModal] = useState(false); - - // Track when "other" mode is selected in the target editor - const [isOtherModeSelected, setIsOtherModeSelected] = useState(false); - - // Track whether user is editing a target or labels. - // null = no user override yet, derive from data; non-null = user's explicit choice. - // Default priority: 1) this case's data, 2) whether the set uses labels, 3) "target". - const [validationTypeOverride, setValidationTypeOverride] = - useState(null); - const caseHasTarget = caseData?.target != null && caseData.target !== ""; - const caseHasLabels = caseData?.labels != null; - const setUsesLabels = validationCases?.some((c) => c.labels != null) ?? false; - const defaultValidationType: ValidationType = caseHasLabels - ? "labels" - : caseHasTarget - ? "target" - : setUsesLabels - ? "labels" - : "target"; - const validationType: ValidationType = - validationTypeOverride ?? defaultValidationType; - - const deleteCaseMutation = useDeleteValidationCase( - editorValidationSetUri ?? "" - ); - - const updateValidationCaseMutation = useUpdateValidationCase( - editorValidationSetUri ?? "" - ); - - // Handler for field changes - updates cache immediately, fires mutation for non-empty values - const handleFieldChange = useCallback( - (field: keyof ValidationCaseRequest, value: JsonValue | string | null) => { - if (!editorValidationSetUri) return; - - // Build the updated case, enforcing mutual exclusivity and predicate rules: - // - Setting target clears labels; setting labels clears target and predicate. - // - Boolean targets clear predicate; "other" targets default predicate to "eq". - const clearOpposite = - field === "target" - ? isOtherTarget(value) - ? { - labels: null, - ...(!caseData?.predicate && { predicate: "eq" as const }), - } - : { labels: null, predicate: null } - : field === "labels" - ? { target: null, predicate: null } - : {}; - const updatedCase: ValidationCase = caseData - ? { ...caseData, ...clearOpposite, [field]: value } - : { - id: transcriptId, - labels: null, - predicate: null, - split: null, - target: null, - [field]: value, - }; - - // Skip save if this is a NEW case with empty target (user selected "Other" but hasn't typed a value yet). - // But allow saving empty target if there was a previous value (user is clearing it). - const hasEmptyTarget = - updatedCase.target == null || updatedCase.target === ""; - const hadPreviousTarget = - caseData?.target != null && caseData.target !== ""; - const isNewEmptyCase = - hasEmptyTarget && updatedCase.labels == null && !hadPreviousTarget; - - if (isNewEmptyCase) { - // Update cache for UI but don't save to server yet - queryClient.setQueryData( - validationQueryKeys.case({ - url: editorValidationSetUri, - caseId: transcriptId, - }), - updatedCase - ); - return; - } - - // Fire mutation immediately - optimistic updates handle UI. - // Include both target and labels so the optimistic cache update - // (which spreads data onto the previous case) clears the opposite field. - const request: ValidationCaseRequest = { - id: updatedCase.id, - target: updatedCase.target, - labels: updatedCase.labels, - predicate: updatedCase.predicate, - split: updatedCase.split, - }; - - setSaveStatus("saving"); - setSaveError(null); - - updateValidationCaseMutation.mutate( - { caseId: transcriptId, data: request }, - { - onSuccess: () => { - setSaveStatus("saved"); - setTimeout(() => setSaveStatus("idle"), 1500); - }, - onError: (error) => { - setSaveStatus("error"); - setSaveError(error.message); - }, - } - ); - }, - [ - editorValidationSetUri, - transcriptId, - caseData, - queryClient, - updateValidationCaseMutation, - ] - ); - - // Handle switching between target and labels mode. - // Only updates which editor is shown — the actual data clearing happens - // server-side when the user saves a value in the new mode, since - // handleFieldChange sends either target or labels, never both. - const handleValidationTypeChange = useCallback( - (newType: string) => { - if (newType !== "target" && newType !== "labels") return; - if (newType === validationType) return; - setValidationTypeOverride(newType); - }, - [validationType] - ); - - const handleValidationSetSelect = useCallback( - (uri: string | undefined) => { - setEditorSelectedValidationSetUri(uri); - setSearchParams( - (prevParams) => updateValidationSetParam(prevParams, uri), - { replace: true } - ); - }, - [setEditorSelectedValidationSetUri, setSearchParams] - ); - - const closeValidationSidebar = useCallback(() => { - setSearchParams((prevParams) => { - const isCurrentlyOpen = getValidationParam(prevParams); - return updateValidationParam(prevParams, !isCurrentlyOpen); - }); - }, [setSearchParams]); - - // Handler for creating a new validation set - const handleCreateSet = useCallback( - async (name: string) => { - setCreateError(null); - - // Validate filename - const validation = isValidFilename(name); - if (!validation.isValid) { - setCreateError(validation.error ?? "Invalid filename"); - return; - } - - // Always use project directory (as URI) for new validation sets - // Only add .csv extension if the user didn't already include a valid extension - const filename = hasValidationSetExtension(name) ? name : `${name}.csv`; - const newUri = `${config.project_dir}/${filename}`; - - // Check for duplicates - if (validationSets?.includes(newUri)) { - setCreateError("A validation set with this name already exists"); - return; - } - - try { - await createSetMutation.mutateAsync({ path: newUri, cases: [] }); - setCreateError(null); - handleValidationSetSelect(newUri); // Select the new set - } catch (err) { - setCreateError( - err instanceof Error ? err.message : "Failed to create set" - ); - } - }, - [ - config.project_dir, - validationSets, - createSetMutation, - handleValidationSetSelect, - ] - ); - - // Handler for deleting the current validation case - const handleDeleteCase = useCallback(async () => { - if (!transcriptId || !editorValidationSetUri) return; - try { - await deleteCaseMutation.mutateAsync(transcriptId); - setShowDeleteModal(false); - // Reset cache to null after deletion (keeps panel open) - queryClient.setQueryData( - validationQueryKeys.case({ - url: editorValidationSetUri, - caseId: transcriptId, - }), - null - ); - } catch { - // Error is handled by mutation state - modal stays open - } - }, [transcriptId, editorValidationSetUri, deleteCaseMutation, queryClient]); - - const isEditable = - caseData?.target === undefined || - caseData?.target === null || - (!Array.isArray(caseData.target) && typeof caseData.target !== "object"); - - const hasCaseData = - (caseData?.target != null && caseData.target !== "") || - caseData?.labels != null; - - const actions: ReactNode = hasCaseData ? ( - { - if (value === "delete") setShowDeleteModal(true); - }} - title="More actions" - /> - ) : undefined; - - return ( -
- -
- - - - void handleCreateSet(name)} - appConfig={config} - /> - {createError && ( -
{createError}
- )} -
- - {!isEditable && ( - <> - - - Validation sets with dictionary or list targets aren't editable - using this UI. - - - )} - {editorValidationSetUri && isEditable && ( - <> - - - handleValidationTypeChange( - (e.target as HTMLInputElement).value - ) - } - > - - - - - - {validationType === "target" && ( - <> - - handleFieldChange("target", target)} - onModeChange={setIsOtherModeSelected} - /> - - - {isOtherModeSelected && ( - - - handleFieldChange("predicate", predicate) - } - existingPredicates={extractUniquePredicates( - validationCases || [] - )} - /> - - )} - - )} - - {validationType === "labels" && ( - - handleFieldChange("labels", labels)} - /> - - )} - - - handleFieldChange("split", split)} - /> - - - setShowDeleteModal(false)} - onConfirm={() => void handleDeleteCase()} - title="Delete Case" - message="Are you sure you want to delete this validation case?" - confirmLabel="Delete" - confirmingLabel="Deleting..." - isConfirming={deleteCaseMutation.isPending} - /> - - )} -
-
- -
- ); -}; - -interface SidebarPanelProps { - children: React.ReactNode; -} - -export const SidebarPanel: FC = ({ children }) => { - return
{children}
; -}; - -interface SidebarHeaderProps { - icon?: string; - title?: string; - secondary?: string; - actions?: React.ReactNode; - onClose?: () => void; -} - -export const SidebarHeader: FC = ({ - icon, - title, - secondary, - actions, - onClose, -}) => { - return ( -
-

- {icon && } - {title} -

- {secondary &&
{secondary}
} - - {(actions || onClose) && ( -
- {actions} - {onClose && ( - - )} -
- )} -
- ); -}; - -export const SecondaryDisplayValue: FC<{ label: string; value: string }> = ({ - label, - value, -}) => { - return ( -
- {label}: - {value} -
- ); -}; - -const InfoBox: FC<{ children: ReactNode }> = ({ children }) => ( -
- -
{children}
-
-); - -type SaveStatusType = "idle" | "saving" | "saved" | "error"; - -interface SaveStatusProps { - status: SaveStatusType; - error: string | null; -} - -const SaveStatus: FC = ({ status, error }) => { - return ( -
- - {status === "saving" - ? "Saving..." - : status === "saved" - ? "Saved" - : status === "error" - ? error || "Error saving changes" - : ""} - -
- ); -}; - -/** - * Returns true if the target is an "other" value (not a boolean or boolean string). - * "Other" targets include numbers, objects, arrays, non-boolean strings, and empty string - * (which indicates the user selected "Other" mode but hasn't typed a value yet). - */ -const isOtherTarget = (target?: JsonValue): boolean => { - if (target === null || target === undefined) { - return false; - } - - // Empty string means "other" was selected but no value typed yet - if (target === "") { - return true; - } - - if (typeof target === "boolean") { - return false; - } - - if (typeof target === "string") { - const lower = target.toLowerCase(); - // "true" and "false" strings are treated as boolean targets - return lower !== "true" && lower !== "false"; - } - - // Numbers, objects, arrays are all "other" targets - return true; -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseLabelsEditor.module.css b/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseLabelsEditor.module.css deleted file mode 100644 index 7dc4c0de3..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseLabelsEditor.module.css +++ /dev/null @@ -1,40 +0,0 @@ -.inputContainer { - display: flex; - flex-wrap: wrap; - gap: 0.3rem; - padding: 4px 6px; - min-height: 30px; - align-items: center; - align-content: flex-start; - background-color: var(--vscode-input-background); - border: 1px solid var(--vscode-input-border, var(--bs-border-color)); - border-radius: 2px; -} - -.labelChip { - /* Label chip specific styles can go here */ -} - -.popoverContent { - display: flex; - flex-direction: column; - gap: 0.5rem; - min-width: 200px; -} - -.popoverField { - display: flex; - flex-direction: column; - gap: 0.2rem; -} - -.popoverLabel { - font-weight: 600; -} - -.popoverActions { - display: flex; - gap: 0.4rem; - justify-content: flex-end; - margin-top: 0.2rem; -} diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseLabelsEditor.tsx b/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseLabelsEditor.tsx deleted file mode 100644 index f157796b3..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseLabelsEditor.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { - VscodeButton, - VscodeRadio, - VscodeRadioGroup, -} from "@vscode-elements/react-elements"; -import clsx from "clsx"; -import { FC, useCallback, useRef, useState } from "react"; - -import { AutocompleteInput } from "../../../components/AutocompleteInput"; -import { ApplicationIcons } from "../../../components/icons"; -import { PopOver } from "../../../components/PopOver"; -import { Chip } from "../../components/Chip"; - -import styles from "./ValidationCaseLabelsEditor.module.css"; - -interface ValidationCaseLabelsEditorProps { - /** Current labels for the validation case (null means no labels set) */ - labels: { [key: string]: boolean } | null; - /** Unique label keys from other cases for autocomplete suggestions */ - availableLabels: string[]; - /** Callback when labels change */ - onChange: (labels: { [key: string]: boolean } | null) => void; -} - -/** - * Editor component for managing labels on a validation case. - * Displays existing labels as chips with remove buttons, - * and provides an "Add" chip to add new labels via popover. - */ -export const ValidationCaseLabelsEditor: FC< - ValidationCaseLabelsEditorProps -> = ({ labels, availableLabels, onChange }) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [newLabelName, setNewLabelName] = useState(""); - const [newLabelValue, setNewLabelValue] = useState("true"); - const addChipRef = useRef(null); - - const labelEntries = labels ? Object.entries(labels) : []; - - const handleToggleLabel = useCallback( - (labelKey: string) => { - if (!labels) return; - onChange({ ...labels, [labelKey]: !labels[labelKey] }); - }, - [labels, onChange] - ); - - const handleRemoveLabel = useCallback( - (labelKey: string) => { - if (!labels) return; - const updated = { ...labels }; - delete updated[labelKey]; - onChange(Object.keys(updated).length === 0 ? null : updated); - }, - [labels, onChange] - ); - - const handleAddLabel = useCallback(() => { - const trimmed = newLabelName.trim(); - if (!trimmed) return; - - const updated = { ...(labels ?? {}), [trimmed]: newLabelValue === "true" }; - onChange(updated); - - setNewLabelName(""); - setNewLabelValue("true"); - setIsPopoverOpen(false); - }, [newLabelName, newLabelValue, labels, onChange]); - - const handleCancel = useCallback(() => { - setNewLabelName(""); - setNewLabelValue("true"); - setIsPopoverOpen(false); - }, []); - - const handleOpenPopover = useCallback(() => { - setNewLabelName(""); - setNewLabelValue("true"); - setIsPopoverOpen(true); - }, []); - - return ( - <> -
- {labelEntries.map(([key, value]) => ( - handleToggleLabel(key)} - onClose={() => handleRemoveLabel(key)} - /> - ))} - -
- - -
-
- - 0} - /> -
- -
- - - setNewLabelValue((e.target as HTMLInputElement).value) - } - > - - - -
- -
- - Cancel - - - Add - -
-
-
- - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCasePredicateSelector.tsx b/src/inspect_scout/_view/www/src/app/validation/components/ValidationCasePredicateSelector.tsx deleted file mode 100644 index a5f2751a8..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCasePredicateSelector.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { - VscodeOption, - VscodeSingleSelect, -} from "@vscode-elements/react-elements"; -import { FC } from "react"; - -import { useDropdownPosition } from "../../../hooks/useDropdownPosition"; -import { ValidationCase } from "../../../types/api-types"; - -import styles from "./ValidationSetSelector.module.css"; - -type Predicate = - | "gt" - | "gte" - | "lt" - | "lte" - | "eq" - | "ne" - | "contains" - | "startswith" - | "endswith" - | "icontains" - | "iequals"; - -const PREDICATES: { value: Predicate; label: string }[] = [ - { value: "eq", label: "Equal (eq)" }, - { value: "ne", label: "Not equal (ne)" }, - { value: "gt", label: "Greater than (gt)" }, - { value: "gte", label: "Greater or equal (gte)" }, - { value: "lt", label: "Less than (lt)" }, - { value: "lte", label: "Less or equal (lte)" }, - { value: "contains", label: "Contains" }, - { value: "startswith", label: "Starts with" }, - { value: "endswith", label: "Ends with" }, - { value: "icontains", label: "Contains (case-insensitive)" }, - { value: "iequals", label: "Equal (case-insensitive)" }, -]; - -interface ValidationCasePredicateSelectorProps { - value: Predicate | null; - onChange: (predicate: Predicate) => void; - disabled?: boolean; - existingPredicates?: Predicate[]; -} - -/** - * Dropdown component for selecting a validation case predicate. - */ -export const ValidationCasePredicateSelector: FC< - ValidationCasePredicateSelectorProps -> = ({ value, onChange, disabled = false, existingPredicates }) => { - const { ref, position } = useDropdownPosition({ - optionCount: PREDICATES.length, - }); - const defaultValue = - existingPredicates && existingPredicates.length === 1 - ? existingPredicates[0] - : "eq"; - - const handleChange = (e: Event) => { - const newValue = (e.target as HTMLSelectElement).value as Predicate; - onChange(newValue); - }; - - return ( -
- - {PREDICATES.map((predicate) => ( - - {predicate.label} - - ))} - -
- ); -}; - -/** - * Extracts unique predicate values from a list of validation cases. - * Returns sorted array of non-empty split values. - */ -export const extractUniquePredicates = ( - cases: ValidationCase[] -): Predicate[] => { - const predicateSet = new Set(); - const predicates = PREDICATES.map((p) => p.value); - - for (const c of cases) { - if (c.predicate && predicates.includes(c.predicate)) { - predicateSet.add(c.predicate); - } - } - return Array.from(predicateSet).sort(); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseTargetEditor.tsx b/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseTargetEditor.tsx deleted file mode 100644 index abae26e36..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCaseTargetEditor.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { - VscodeRadio, - VscodeRadioGroup, - VscodeTextfield, -} from "@vscode-elements/react-elements"; -import { FC, useEffect, useState } from "react"; - -import { JsonValue } from "../../../types/api-types"; -import { useDebouncedCallback } from "../../../utils/useDebouncedCallback"; - -type TargetMode = "true" | "false" | "other" | "unset"; - -/** - * Determines the mode based on the target value. - * - null/undefined = "unset" (user hasn't selected anything) - * - "" (empty string) = "other" (user selected "Other" but hasn't typed a value) - * - "true"/true = "true" - * - "false"/false = "false" - * - any other value = "other" - */ -function getTargetMode(target?: JsonValue): TargetMode { - if (target === undefined || target === null) { - return "unset"; - } - if (target === "true" || target === true) { - return "true"; - } - if (target === "false" || target === false) { - return "false"; - } - // Empty string or any other value = "other" - return "other"; -} - -interface ValidationCaseTargetEditorProps { - target?: JsonValue; - onChange: (newTarget: string) => void; - /** Called when the mode changes (e.g., user selects "other" radio) */ - onModeChange?: (isOtherMode: boolean) => void; -} - -/** - * Editor component for modifying the target of a validation case. - */ -export const ValidationCaseTargetEditor: FC< - ValidationCaseTargetEditorProps -> = ({ target, onChange, onModeChange }) => { - const [mode, setMode] = useState(() => getTargetMode(target)); - - // Notify parent when mode changes - useEffect(() => { - onModeChange?.(mode === "other"); - }, [mode, onModeChange]); - const [customValue, setCustomValue] = useState(() => - getTargetMode(target) === "other" ? String(target ?? "") : "" - ); - // Track when user is typing to prevent sync from overwriting during debounce - const [isTyping, setIsTyping] = useState(false); - - // Sync mode and customValue when target prop changes externally - // (e.g., data loads, switching cases, or after our save completes) - // Skip sync while user is typing to avoid overwriting during debounce - useEffect(() => { - if (isTyping) return; - - // Only sync when target has a valid value. - // The initial "unset" state is handled by useState. - if (target === undefined || target === null) return; - - const newMode = getTargetMode(target); - // TODO: lint react-hooks/set-state-in-effect - consider if fixing this violation makes sense - /* eslint-disable react-hooks/set-state-in-effect */ - setMode(newMode); - if (newMode === "other") { - setCustomValue(String(target ?? "")); - } - /* eslint-enable react-hooks/set-state-in-effect */ - }, [target, isTyping]); - - // Debounce only the text input changes - const debouncedOnChange = useDebouncedCallback((value: unknown) => { - // Call onChange first to update the cache before clearing the typing flag. - onChange(value as string); - setIsTyping(false); - }, 600); - - const handleRadioChange = (value: string) => { - const newMode = value as TargetMode; - setMode(newMode); - - if (newMode === "other") { - // When switching to "other", propagate empty string as sentinel immediately - // This distinguishes "other selected" from "never selected" - onChange(customValue || ""); - } else { - // Radio changes fire immediately (no debounce) - onChange(newMode); - } - }; - - const handleCustomValueChange = (value: string) => { - setIsTyping(true); // Set typing flag to prevent sync during debounce - setCustomValue(value); // Update UI immediately - debouncedOnChange(value); // Debounce the parent callback - }; - - return ( -
- - handleRadioChange((e.target as HTMLInputElement).value) - } - > - - - - - - {mode === "other" && ( - - handleCustomValueChange((e.target as HTMLInputElement).value) - } - style={{ marginTop: "8px" }} - /> - )} -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCasesList.module.css b/src/inspect_scout/_view/www/src/app/validation/components/ValidationCasesList.module.css deleted file mode 100644 index 028636c15..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCasesList.module.css +++ /dev/null @@ -1,165 +0,0 @@ -.container { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - height: 100%; - width: 100%; -} - -/* Grid layout for header and rows */ -.gridContainer { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - overflow: hidden; -} - -/* Grid header - fully enclosed */ -.header { - display: grid; - /* grid-template-columns set dynamically via inline style */ - align-items: center; - gap: 12px; - padding: 0 1rem; - height: 28px; - background: var(--vscode-sideBarSectionHeader-background); - border-bottom: solid var(--bs-light-border-subtle) 1px; - font-size: 0.75rem; - font-weight: 500; - line-height: 28px; - color: var(--vscode-descriptionForeground); - text-transform: uppercase; - letter-spacing: 0.5px; - position: sticky; - top: 0; - z-index: 1; - user-select: none; -} - -.headerCheckbox { - display: flex; - align-items: center; - justify-content: center; - transform: scale(0.85); -} - -.headerTranscript { - display: flex; - align-items: center; - gap: 12px; -} - -/* Bulk actions in header */ -.bulkActions { - display: inline-flex; - align-items: center; - gap: 1px; - margin-left: 12px; -} - -.selectedCount { - font-weight: 600; - text-transform: none; - letter-spacing: normal; - color: var(--vscode-foreground); - margin-right: 8px; -} - -.bulkButton { - text-transform: none; - letter-spacing: normal; - padding: 2px 4px !important; - font-size: 10px !important; - height: 24px !important; - min-height: 24px !important; -} - -.bulkActions :global(vscode-button)::part(base) { - padding: 2px 6px !important; -} - -.bulkButton i { - margin-right: 4px; -} - -/* Responsive: Icons only at narrow widths */ -@media (max-width: 900px) { - .buttonText { - display: none; - } - - .bulkButton i { - margin-right: 0; - } - - .selectedCount { - margin-right: 4px; - } -} - -.headerLabels { - text-align: right; - min-width: 80px; - padding-right: 8px; -} - -.headerTarget { - text-align: right; - min-width: 80px; - padding-right: 8px; -} - -.headerSplit { - text-align: center; -} - -.headerActions { - display: flex; - justify-content: flex-start; - min-width: 56px; -} - -/* Scrollable list */ -.list { - display: flex; - flex-direction: column; - gap: 0; - padding: 0; - overflow-y: auto; - flex: 1; -} - -.emptyState { - color: var(--vscode-descriptionForeground); - text-align: center; - padding: 24px 1rem; -} - -/* Modal styles */ -.modalContent { - display: flex; - flex-direction: column; - gap: 12px; -} - -.modalContent p { - margin: 0; - color: var(--vscode-foreground); -} - -.splitSelector { - display: flex; - flex-direction: column; - gap: 8px; -} - -.customInput { - margin-top: 4px; -} - -.warning { - color: var(--vscode-errorForeground); - font-size: 0.9em; -} diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCasesList.tsx b/src/inspect_scout/_view/www/src/app/validation/components/ValidationCasesList.tsx deleted file mode 100644 index dc6c64c36..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationCasesList.tsx +++ /dev/null @@ -1,437 +0,0 @@ -import { VscodeButton, VscodeCheckbox } from "@vscode-elements/react-elements"; -import { CSSProperties, FC, useCallback, useMemo, useState } from "react"; - -import { ApplicationIcons } from "../../../components/icons"; -import { Modal } from "../../../components/Modal"; -import { useStore } from "../../../state/store"; -import { TranscriptInfo, ValidationCase } from "../../../types/api-types"; -import { useTranscriptsByIds } from "../hooks/useTranscriptsByIds"; -import { extractUniqueSplits, getCaseKey, getIdText } from "../utils"; - -import { CopyMoveCasesModal } from "./CopyMoveCasesModal"; -import { ValidationCaseCard } from "./ValidationCaseCard"; -import styles from "./ValidationCasesList.module.css"; -import { ValidationSplitSelector } from "./ValidationSplitSelector"; - -interface ValidationCasesListProps { - cases: ValidationCase[]; - transcriptsDir: string | undefined; - sourceUri: string | undefined; - onBulkSplitChange?: (ids: string[], split: string | null) => void; - onBulkDelete?: (ids: string[]) => void; - onSingleSplitChange?: (caseId: string, split: string | null) => void; - onSingleDelete?: (caseId: string) => void; - isUpdating?: boolean; - isDeleting?: boolean; -} - -/** - * List component for displaying and managing validation cases. - * Includes filtering, selection, and individual case cards. - */ -export const ValidationCasesList: FC = ({ - cases, - transcriptsDir, - sourceUri, - onBulkSplitChange, - onBulkDelete, - onSingleSplitChange, - onSingleDelete, - isUpdating, - isDeleting, -}) => { - // State from store - const selection = useStore((state) => state.validationCaseSelection); - const setSelection = useStore((state) => state.setValidationCaseSelection); - const toggleSelection = useStore( - (state) => state.toggleValidationCaseSelection - ); - const splitFilter = useStore((state) => state.validationSplitFilter); - const searchText = useStore((state) => state.validationSearchText); - - // Extract all transcript IDs from cases (use first ID for composite IDs) - const transcriptIds = useMemo(() => { - return cases - .map((c) => (Array.isArray(c.id) ? c.id[0] : c.id)) - .filter((id): id is string => id !== undefined); - }, [cases]); - - // Extract unique splits from all cases - const existingSplits = useMemo(() => extractUniqueSplits(cases), [cases]); - - // Determine which columns to show based on case data - const hasLabels = useMemo( - () => cases.some((c) => c.labels !== null && c.labels !== undefined), - [cases] - ); - const hasTargets = useMemo( - () => cases.some((c) => c.target !== null && c.target !== undefined), - [cases] - ); - - // Build dynamic grid columns: checkbox, transcript, [labels], [target], split, actions - const gridColumns = useMemo(() => { - const cols = ["20px", "1fr"]; // checkbox, transcript - if (hasLabels) cols.push("auto"); // labels - if (hasTargets) cols.push("auto"); // target - cols.push("90px", "auto"); // split, actions - return cols.join(" "); - }, [hasLabels, hasTargets]); - - const gridStyle: CSSProperties = { gridTemplateColumns: gridColumns }; - - // Fetch transcript data for all cases - const { - data: transcriptMap, - sourceIds, - loading: transcriptsLoading, - } = useTranscriptsByIds(transcriptsDir, transcriptIds); - - // Safe transcript lookup that detects staleness - // Returns undefined if the map is stale (built for different IDs than requested) - const getTranscriptSafely = useCallback( - (transcriptId: string | undefined): TranscriptInfo | undefined => { - if (!transcriptId) return undefined; - if (!transcriptMap) return undefined; - - // Check for staleness: the ID should be in the set of IDs the map was built for - if (sourceIds && !sourceIds.has(transcriptId)) { - console.warn( - `[ValidationCasesList] Stale transcript lookup detected: ` + - `ID "${transcriptId}" not in sourceIds. ` + - `sourceIds: [${[...sourceIds].join(", ")}], ` + - `requested: [${transcriptIds.join(", ")}]` - ); - return undefined; - } - - return transcriptMap.get(transcriptId); - }, - [transcriptMap, sourceIds, transcriptIds] - ); - - // Filter and sort cases based on split and search - const filteredCases = useMemo(() => { - const filtered = cases.filter((c) => { - // Split filter - if (splitFilter && c.split !== splitFilter) { - return false; - } - - // Search filter (search across multiple fields) - if (searchText) { - const search = searchText.toLowerCase(); - const transcriptId = Array.isArray(c.id) ? c.id[0] : c.id; - const transcript = getTranscriptSafely(transcriptId); - - // Check ID - const idText = getIdText(c.id).toLowerCase(); - if (idText.includes(search)) return true; - - // Check transcript details if available - if (transcript) { - if (transcript.task_set?.toLowerCase().includes(search)) return true; - if (transcript.task_id?.toLowerCase().includes(search)) return true; - if (transcript.model?.toLowerCase().includes(search)) return true; - if (transcript.agent?.toLowerCase().includes(search)) return true; - } - - return false; - } - - return true; - }); - - // Sort by split: no split first, then alphabetically by split name - return filtered.sort((a, b) => { - const splitA = a.split; - const splitB = b.split; - - // No split comes first - if (!splitA && splitB) return -1; - if (splitA && !splitB) return 1; - if (!splitA && !splitB) return 0; - - // Both have splits - sort alphabetically - return splitA!.localeCompare(splitB!); - }); - }, [cases, splitFilter, searchText, getTranscriptSafely]); - - // Get filtered case keys for select all logic - const filteredCaseKeys = useMemo(() => { - return filteredCases.map((c) => getCaseKey(c.id)); - }, [filteredCases]); - - // Get selected IDs - const selectedIds = useMemo(() => { - return Object.entries(selection) - .filter(([, selected]) => selected) - .map(([id]) => id); - }, [selection]); - - // Get selected cases (full case data for copy/move operations) - const selectedCases = useMemo(() => { - return filteredCases.filter((c) => selection[getCaseKey(c.id)]); - }, [filteredCases, selection]); - - // Check if all filtered cases are selected - const allSelected = - filteredCaseKeys.length > 0 && - filteredCaseKeys.every((key) => selection[key]); - - // Check if some (but not all) filtered cases are selected - const someSelected = - filteredCaseKeys.some((key) => selection[key]) && !allSelected; - - // Handle select all / deselect all - const handleSelectAllChange = () => { - if (allSelected) { - // Deselect all filtered cases - const newSelection = { ...selection }; - filteredCaseKeys.forEach((key) => { - delete newSelection[key]; - }); - setSelection(newSelection); - } else { - // Select all filtered cases - const newSelection = { ...selection }; - filteredCaseKeys.forEach((key) => { - newSelection[key] = true; - }); - setSelection(newSelection); - } - }; - - // Handle bulk split change - clear selection after - const handleBulkSplitChange = (ids: string[], split: string | null) => { - onBulkSplitChange?.(ids, split); - setSelection({}); - }; - - // Handle bulk delete - clear selection after - const handleBulkDelete = (ids: string[]) => { - onBulkDelete?.(ids); - setSelection({}); - }; - - // Bulk action modal state - single enum instead of multiple booleans - type ModalType = "none" | "delete" | "split" | "copy" | "move"; - const [activeModal, setActiveModal] = useState("none"); - const [bulkSplitValue, setBulkSplitValue] = useState(null); - - const handleAssignSplit = () => { - handleBulkSplitChange(selectedIds, bulkSplitValue); - setActiveModal("none"); - setBulkSplitValue(null); - }; - - const handleConfirmDelete = () => { - handleBulkDelete(selectedIds); - setActiveModal("none"); - }; - - const closeModal = () => setActiveModal("none"); - - const hasSelection = selectedIds.length > 0; - const hasBulkActions = onBulkSplitChange && onBulkDelete; - - return ( -
- {/* Grid container with sticky header */} -
- {/* Header row */} -
-
- -
-
- Transcript - {/* Bulk actions inline */} - {hasSelection && hasBulkActions && ( - - - {selectedIds.length} selected - - setActiveModal("split")} - disabled={isUpdating || isDeleting} - className={styles.bulkButton} - title="Assign Split" - > - - Assign Split - - setActiveModal("copy")} - disabled={isUpdating || isDeleting} - className={styles.bulkButton} - title="Copy" - > - - Copy - - setActiveModal("move")} - disabled={isUpdating || isDeleting} - className={styles.bulkButton} - title="Move" - > - - Move - - setActiveModal("delete")} - disabled={isUpdating || isDeleting} - className={styles.bulkButton} - title="Delete" - > - - Delete - - - )} -
- {hasLabels &&
Labels
} - {hasTargets &&
Target
} -
Split
-
Actions
-
- - {/* Scrollable list */} -
- {transcriptsLoading ? ( -
- Loading transcript details... -
- ) : filteredCases.length === 0 ? ( -
- {cases.length === 0 - ? "No validation cases in this set." - : "No cases match the current filters."} -
- ) : ( - filteredCases.map((c) => { - const caseKey = getCaseKey(c.id); - const transcriptId = Array.isArray(c.id) ? c.id[0] : c.id; - const transcript = getTranscriptSafely(transcriptId); - return ( - toggleSelection(caseKey)} - existingSplits={existingSplits} - onSplitChange={ - onSingleSplitChange - ? (split) => onSingleSplitChange(caseKey, split) - : undefined - } - onDelete={ - onSingleDelete ? () => onSingleDelete(caseKey) : undefined - } - isUpdating={isUpdating} - isDeleting={isDeleting} - showLabels={hasLabels} - showTarget={hasTargets} - gridStyle={gridStyle} - /> - ); - }) - )} -
-
- - {/* Split Assignment Modal */} - - - Cancel - - - {isUpdating ? "Assigning..." : "Assign"} - - - } - > -
-

- Assign a split to {selectedIds.length} selected{" "} - {selectedIds.length === 1 ? "case" : "cases"}. -

- -
- -
-
-
- - {/* Delete Confirmation Modal */} - - - Cancel - - - {isDeleting ? "Deleting..." : "Delete"} - - - } - > -
-

- Are you sure you want to delete {selectedIds.length}{" "} - {selectedIds.length === 1 ? "case" : "cases"}? -

-

This action cannot be undone.

-
-
- - {/* Copy Cases Modal */} - {sourceUri && ( - setSelection({})} - /> - )} - - {/* Move Cases Modal */} - {sourceUri && ( - setSelection({})} - /> - )} -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationFilterBar.module.css b/src/inspect_scout/_view/www/src/app/validation/components/ValidationFilterBar.module.css deleted file mode 100644 index ccb74a794..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationFilterBar.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.container { - display: flex; - align-items: center; - gap: 12px; - padding: 0.5rem 1rem; - border-bottom: solid var(--bs-light-border-subtle) 1px; -} - -.searchInput { - width: 200px; - font-size: 0.85rem !important; -} - -.searchInput input { - font-size: 0.85rem !important; -} - -.filterGroup { - display: flex; - align-items: center; - gap: 8px; -} - -.filterLabel { - color: var(--vscode-descriptionForeground); - font-size: 0.85rem; -} - -.splitSelect { - width: 90px !important; - min-width: 90px !important; - max-width: 90px !important; -} diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationFilterBar.tsx b/src/inspect_scout/_view/www/src/app/validation/components/ValidationFilterBar.tsx deleted file mode 100644 index 4bfb9bd92..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationFilterBar.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { - VscodeOption, - VscodeSingleSelect, -} from "@vscode-elements/react-elements"; -import { ChangeEvent, FC, useMemo } from "react"; - -import { ApplicationIcons } from "../../../components/icons"; -import { TextInput } from "../../../components/TextInput"; -import { ValidationCase } from "../../../types/api-types"; -import { extractUniqueSplits } from "../utils"; - -import styles from "./ValidationFilterBar.module.css"; - -interface ValidationFilterBarProps { - cases: ValidationCase[]; - splitFilter: string | undefined; - onSplitFilterChange: (split: string | undefined) => void; - searchText: string | undefined; - onSearchTextChange: (text: string | undefined) => void; -} - -/** - * Filter bar with split dropdown and ID search. - */ -export const ValidationFilterBar: FC = ({ - cases, - splitFilter, - onSplitFilterChange, - searchText, - onSearchTextChange, -}) => { - // Extract unique splits from cases - const splits = useMemo(() => extractUniqueSplits(cases), [cases]); - - const handleSplitChange = (e: Event) => { - const value = (e.target as HTMLSelectElement).value; - onSplitFilterChange(value || undefined); - }; - - const handleSearchChange = (e: ChangeEvent) => { - onSearchTextChange(e.target.value || undefined); - }; - - return ( -
- {/* Search first */} - - - {/* Filter second */} -
- Split: - - All splits - {splits.map((split) => ( - - {split} - - ))} - -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSetSelector.module.css b/src/inspect_scout/_view/www/src/app/validation/components/ValidationSetSelector.module.css deleted file mode 100644 index 3c147fb97..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSetSelector.module.css +++ /dev/null @@ -1,216 +0,0 @@ -.container { - position: relative; - display: flex; - flex-direction: column; -} - -/* Trigger button (collapsed state) */ -.trigger { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - width: fit-content; - padding: 3px 3px; - background: var(--vscode-dropdown-background, var(--vscode-input-background)); - border: 1px solid - var(--vscode-dropdown-border, var(--vscode-input-border, #3c3c3c)); - border-radius: 2px; - color: var(--vscode-dropdown-foreground, var(--vscode-foreground)); - cursor: pointer; - text-align: left; - font-family: inherit; - font-size: inherit; -} - -.chevron { - flex-shrink: 0; - font-size: 12px; - opacity: 0.6; - line-height: 1; - transform: rotate(180deg); -} - -.trigger:hover { - background: var(--vscode-dropdown-background); - border-color: var(--vscode-focusBorder); -} - -.trigger:focus { - outline: none; - border-color: var(--vscode-focusBorder); -} - -.triggerContent { - display: flex; - flex-direction: column; -} - -.triggerSizer { - font-size: var(--vscode-font-size, 13px); - white-space: nowrap; - height: 0; - overflow: hidden; - display: block; -} - -.triggerPrimary { - font-size: var(--vscode-font-size, 13px); - white-space: nowrap; -} - -.triggerSecondary { - font-size: calc(var(--vscode-font-size, 13px) - 2px); - color: var(--vscode-descriptionForeground); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - opacity: 0.7; -} - -.triggerPlaceholder { - color: var(--vscode-input-placeholderForeground); -} - -/* Dropdown panel - rendered via portal */ -.dropdown { - z-index: 10000; - background: var( - --vscode-dropdown-background, - var(--vscode-input-background, #252526) - ); - border: 1px solid - var(--vscode-dropdown-border, var(--vscode-input-border, #3c3c3c)); - border-radius: 2px; - max-height: 200px; - overflow-y: auto; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); -} - -/* Dropdown items */ -.item { - padding: 6px 10px; - cursor: pointer; - border-bottom: 1px solid var(--vscode-panel-border); -} - -.item:last-child { - border-bottom: none; -} - -.item:hover { - background-color: var(--vscode-list-hoverBackground); -} - -/* Selected item - with proper contrast */ -.item.selected { - background-color: var(--vscode-list-activeSelectionBackground); -} - -.item.selected .primaryText, -.item.selected .secondaryText { - color: var(--vscode-list-activeSelectionForeground); -} - -.primaryText { - color: var(--vscode-foreground); - font-size: var(--vscode-font-size, 13px); -} - -.secondaryText { - color: var(--vscode-descriptionForeground); - font-size: calc(var(--vscode-font-size, 13px) - 2px); - margin-top: 1px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - opacity: 0.7; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.loading { - color: var(--vscode-descriptionForeground); - font-style: italic; -} - -.error { - color: var(--vscode-errorForeground); -} - -/* Divider between sets and create option */ -.divider { - height: 1px; - background: var(--vscode-editorWidget-border, var(--vscode-panel-border)); - margin: 4px 10px; -} - -/* Remove border from item before divider */ -.item:has(+ .divider) { - border-bottom: none; -} - -/* Create option styling */ -.createOption { - margin-top: 2px; -} - -.createOption .primaryText { - color: var(--vscode-textLink-foreground); - display: flex; - align-items: center; - gap: 6px; -} - -.createOption .primaryText::before { - content: "+"; - font-weight: 600; - font-size: 14px; -} - -/* Modal content styles */ -.modalContent { - display: flex; - flex-direction: column; - gap: 12px; -} - -.modalContent p { - margin: 0; -} - -.hint { - font-size: 0.8rem; - color: var(--vscode-descriptionForeground); - font-style: italic; -} - -/* Modal button styles */ -.modalButton { - padding: 6px 14px; - border: 1px solid var(--vscode-button-border, transparent); - border-radius: 2px; - background: var(--vscode-button-secondaryBackground); - color: var(--vscode-button-secondaryForeground); - cursor: pointer; - font-size: 13px; -} - -.modalButton:hover { - background: var(--vscode-button-secondaryHoverBackground); -} - -.modalButton:disabled { - opacity: 0.5; - cursor: default; -} - -.modalButtonPrimary { - background: var(--vscode-button-background); - color: var(--vscode-button-foreground); -} - -.modalButtonPrimary:hover { - background: var(--vscode-button-hoverBackground); -} diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSetSelector.tsx b/src/inspect_scout/_view/www/src/app/validation/components/ValidationSetSelector.tsx deleted file mode 100644 index c5fd3d143..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSetSelector.tsx +++ /dev/null @@ -1,386 +0,0 @@ -import { VscodeTextfield } from "@vscode-elements/react-elements"; -import { FC, useState, useRef, useEffect, useMemo } from "react"; -import { createPortal } from "react-dom"; - -import { Modal } from "../../../components/Modal"; -import { AppConfig } from "../../../types/api-types"; -import { dirname } from "../../../utils/path"; -import { projectOrAppAliasedPath } from "../../server/useAppConfig"; -import { - getFilenameFromUri, - hasValidationSetExtension, - VALIDATION_SET_EXTENSIONS, -} from "../utils"; - -import styles from "./ValidationSetSelector.module.css"; - -interface ValidationSetSelectorProps { - validationSets: string[]; - selectedUri: string | undefined; - onSelect: (uri: string | undefined) => void; - /** Size trigger to fit longest option (default: false) */ - autoSize?: boolean; - - /** Adds a create new option the dropown */ - allowCreate?: boolean; - onCreate?: (name: string) => void; - - /** Project directory path for displaying full file path in create modal */ - appConfig?: AppConfig; -} - -/** - * Select-box component for selecting validation sets. - * Shows collapsed trigger with 2-line display (filename + path). - * Opens dropdown on click with keyboard navigation support. - */ -export const ValidationSetSelector: FC = ({ - validationSets, - selectedUri, - onSelect, - autoSize = false, - allowCreate = false, - onCreate, - appConfig, -}) => { - const [isOpen, setIsOpen] = useState(false); - const [dropdownStyle, setDropdownStyle] = useState({}); - const containerRef = useRef(null); - const triggerRef = useRef(null); - const dropdownRef = useRef(null); - - // Modal state for creating new set - const [showCreateModal, setShowCreateModal] = useState(false); - const [newSetName, setNewSetName] = useState(""); - const [validationError, setValidationError] = useState(null); - - // Extract display name from URI (last part of path with extension) - const getDisplayName = (uri: string): string => { - return getFilenameFromUri(uri); - }; - - // Find the longest display name for sizing - const longestDisplayName = useMemo(() => { - if (validationSets.length === 0) return ""; - return validationSets.reduce((longest, uri) => { - const name = getDisplayName(uri); - return name.length > longest.length ? name : longest; - }, ""); - }, [validationSets]); - - const getDisplayPath = (uri: string, appConfig?: AppConfig): string => { - let path = appConfig - ? projectOrAppAliasedPath(appConfig, dirname(uri)) - : dirname(uri); - // Strip file:// prefix for cleaner display - if (path && path.startsWith("file://")) { - path = path.slice(7); - } - return path ?? uri; - }; - - // Update dropdown position when opening - useEffect(() => { - if (isOpen && triggerRef.current) { - const rect = triggerRef.current.getBoundingClientRect(); - setDropdownStyle({ - position: "fixed", - top: rect.bottom + 2, - left: rect.left, - width: rect.width, - }); - } - }, [isOpen]); - - // Close dropdown when trigger resizes to prevent orphaned positioning - useEffect(() => { - if (!isOpen || !triggerRef.current) return; - - let initialCall = true; - const observer = new ResizeObserver(() => { - // Skip the initial callback that fires when observe() is called - if (initialCall) { - initialCall = false; - return; - } - setIsOpen(false); - }); - - observer.observe(triggerRef.current); - return () => observer.disconnect(); - }, [isOpen]); - - // Close on click outside (check both container and dropdown since dropdown is in portal) - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - const target = e.target as Node; - const isOutsideContainer = - containerRef.current && !containerRef.current.contains(target); - const isOutsideDropdown = - dropdownRef.current && !dropdownRef.current.contains(target); - - if (isOutsideContainer && isOutsideDropdown) { - setIsOpen(false); - } - }; - if (isOpen) { - document.addEventListener("mousedown", handleClickOutside); - return () => - document.removeEventListener("mousedown", handleClickOutside); - } - }, [isOpen]); - - const handleSelect = (uri: string) => { - if (uri === "__create_new__") { - setShowCreateModal(true); - setNewSetName(""); - setValidationError(null); - setIsOpen(false); - } else { - onSelect(uri); - setIsOpen(false); - } - }; - - // Check if a name has an extension (any extension, valid or not) - const hasAnyExtension = (name: string): boolean => { - const lastDot = name.lastIndexOf("."); - // Has extension if there's a dot that's not at the start and has chars after it - return lastDot > 0 && lastDot < name.length - 1; - }; - - // Check if a name is starting to type an extension (has a dot) - const isTypingExtension = (name: string): boolean => { - const lastDot = name.lastIndexOf("."); - return lastDot > 0; // Has a dot that's not at the start - }; - - // Check if the partial extension could be the start of a valid extension - const isValidPartialExtension = (name: string): boolean => { - const lastDot = name.lastIndexOf("."); - if (lastDot <= 0) return true; // No extension being typed - - const partialExt = name.slice(lastDot).toLowerCase(); - // Check if any valid extension starts with what the user has typed - return VALIDATION_SET_EXTENSIONS.some((ext) => ext.startsWith(partialExt)); - }; - - // Get the extension validation error for real-time feedback - const getExtensionError = (name: string): string | null => { - const trimmed = name.trim(); - if (!trimmed) return null; - - // If user is typing an extension that can't match any valid extension - if (isTypingExtension(trimmed) && !isValidPartialExtension(trimmed)) { - return `Invalid extension. Valid extensions: ${VALIDATION_SET_EXTENSIONS.join(", ")}`; - } - - return null; - }; - - // Modal handlers - const handleNameInput = (e: Event) => { - setNewSetName((e.target as HTMLInputElement).value); - setValidationError(null); - }; - - const handleCreateSubmit = () => { - const trimmedName = newSetName.trim(); - if (!trimmedName) return; - - // Check if user provided an extension that's not valid - if ( - hasAnyExtension(trimmedName) && - !hasValidationSetExtension(trimmedName) - ) { - setValidationError( - `Invalid extension. Valid extensions: ${VALIDATION_SET_EXTENSIONS.join(", ")}` - ); - return; - } - - onCreate?.(trimmedName); - setShowCreateModal(false); - setNewSetName(""); - setValidationError(null); - }; - - const handleModalClose = () => { - setShowCreateModal(false); - setNewSetName(""); - setValidationError(null); - }; - - // Keyboard navigation - const handleKeyDown = (e: React.KeyboardEvent) => { - if (!isOpen) { - if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") { - e.preventDefault(); - setIsOpen(true); - } - return; - } - - const currentIndex = selectedUri ? validationSets.indexOf(selectedUri) : -1; - - if (e.key === "ArrowDown") { - e.preventDefault(); - const nextIndex = Math.min(currentIndex + 1, validationSets.length - 1); - onSelect(validationSets[nextIndex]); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - const prevIndex = Math.max(currentIndex - 1, 0); - onSelect(validationSets[prevIndex]); - } else if (e.key === "Escape") { - setIsOpen(false); - } else if (e.key === "Enter") { - setIsOpen(false); - } - }; - - // If any of the items will show a non-root dir, show paths - // spacing for all of them - const hasNonRootDir = validationSets.some((uri) => { - return !!getDisplayPath(uri, appConfig); - }); - - const dropdown = isOpen ? ( -
- {validationSets.map((uri) => ( -
handleSelect(uri)} - > -
{getDisplayName(uri)}
-
- {getDisplayPath(uri, appConfig) || (hasNonRootDir ? "\u00A0" : "")} -
-
- ))} - - {/* Create new set option */} - {allowCreate && onCreate && ( - <> - {validationSets.length > 0 &&
} -
handleSelect("__create_new__")} - > -
Create new set...
-
- - )} -
- ) : null; - - return ( - <> -
- {/* Trigger button - shows selected item */} - - - {/* Dropdown rendered via portal to escape clipping */} - {createPortal(dropdown, document.body)} -
- - {/* Create new set modal */} - - - - - } - > -
-

Enter a name for the new validation set:

- - {(() => { - const trimmedName = newSetName.trim(); - const extensionError = getExtensionError(trimmedName); - const displayError = validationError || extensionError; - const displayDir = appConfig?.project_dir?.startsWith("file://") - ? appConfig?.project_dir.slice(7) - : appConfig?.project_dir; - - // Show hint only if no error and we have a name and projectDir - if (trimmedName && !displayError && displayDir) { - // If user is providing any extension, use their filename as-is - // Otherwise append .csv - const filename = isTypingExtension(trimmedName) - ? trimmedName - : `${trimmedName}.csv`; - return ( - - {displayDir}/{filename} - - ); - } - - // Show error if present - if (displayError) { - return {displayError}; - } - - return null; - })()} -
-
- - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSplitSelector.module.css b/src/inspect_scout/_view/www/src/app/validation/components/ValidationSplitSelector.module.css deleted file mode 100644 index d074efa8d..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSplitSelector.module.css +++ /dev/null @@ -1,38 +0,0 @@ -/* Modal styles for the "New Split" dialog */ -.modalContent { - display: flex; - flex-direction: column; - gap: 12px; -} - -.modalContent p { - margin: 0; -} - -.modalButton { - padding: 6px 14px; - border: 1px solid var(--vscode-button-border, transparent); - border-radius: 2px; - background: var(--vscode-button-secondaryBackground); - color: var(--vscode-button-secondaryForeground); - cursor: pointer; - font-size: 13px; -} - -.modalButton:hover { - background: var(--vscode-button-secondaryHoverBackground); -} - -.modalButtonPrimary { - background: var(--vscode-button-background); - color: var(--vscode-button-foreground); -} - -.modalButtonPrimary:hover { - background: var(--vscode-button-hoverBackground); -} - -.modalButtonPrimary:disabled { - opacity: 0.5; - cursor: default; -} diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSplitSelector.tsx b/src/inspect_scout/_view/www/src/app/validation/components/ValidationSplitSelector.tsx deleted file mode 100644 index 76d97ba9d..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSplitSelector.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { - VscodeOption, - VscodeSingleSelect, - VscodeTextfield, -} from "@vscode-elements/react-elements"; -import { FC, useMemo, useState } from "react"; - -import { Modal } from "../../../components/Modal"; -import { useDropdownPosition } from "../../../hooks/useDropdownPosition"; - -import styles from "./ValidationSplitSelector.module.css"; - -interface ValidationSplitSelectorProps { - /** Current split value (null means no split) */ - value: string | null; - /** Available splits for the dropdown */ - existingSplits: string[]; - /** Callback when split changes */ - onChange: (split: string | null) => void; - /** Disable the selector */ - disabled?: boolean; - /** Additional className for the select element */ - className?: string; - /** Label for "no split" option */ - noSplitLabel?: string; - /** Label for "new split" option */ - newSplitLabel?: string; -} - -/** - * Reusable split selector component with built-in "New Split" modal. - * Style-agnostic: consumers control styling via className prop. - */ -export const ValidationSplitSelector: FC = ({ - value, - existingSplits, - onChange, - disabled = false, - className, - noSplitLabel = "(Optional)", - newSplitLabel = "New split...", -}) => { - // Internal modal state - const [showCustomModal, setShowCustomModal] = useState(false); - const [customSplitValue, setCustomSplitValue] = useState(""); - - // Ensure current value is always in the options list. - // This prevents the dropdown from showing "(No split)" during React Query - // cache transitions when the cases data is temporarily stale. - const effectiveSplits = useMemo(() => { - if (value && !existingSplits.includes(value)) { - return [...existingSplits, value].sort(); - } - return existingSplits; - }, [existingSplits, value]); - - // Auto-position dropdown based on available viewport space - // Option count: 1 (no split) + splits + 1 (new split) - const { ref, position } = useDropdownPosition({ - optionCount: effectiveSplits.length + 2, - }); - - // Map null to internal sentinel value for the select - const selectValue = value ?? "__none__"; - - const handleSelectChange = (e: Event) => { - const newValue = (e.target as HTMLSelectElement).value; - if (newValue === "__custom__") { - setShowCustomModal(true); - setCustomSplitValue(""); - } else if (newValue === "__none__") { - onChange(null); - } else { - onChange(newValue); - } - }; - - const handleCustomInput = (e: Event) => { - setCustomSplitValue((e.target as HTMLInputElement).value); - }; - - const handleCustomSubmit = () => { - if (customSplitValue.trim()) { - onChange(customSplitValue.trim()); - } - setShowCustomModal(false); - setCustomSplitValue(""); - }; - - const handleModalClose = () => { - setShowCustomModal(false); - setCustomSplitValue(""); - }; - - return ( - <> -
- - {noSplitLabel} - {effectiveSplits.map((s) => ( - - {s} - - ))} - {newSplitLabel} - -
- - - - - - } - > -
-

Enter a name for the new split:

- -
-
- - ); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSummary.module.css b/src/inspect_scout/_view/www/src/app/validation/components/ValidationSummary.module.css deleted file mode 100644 index ab838b76b..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSummary.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.container { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - color: var(--vscode-descriptionForeground); - font-size: 14px; -} - -.separator { - color: var(--vscode-panel-border); -} diff --git a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSummary.tsx b/src/inspect_scout/_view/www/src/app/validation/components/ValidationSummary.tsx deleted file mode 100644 index bf7dba998..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/components/ValidationSummary.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { FC, useMemo } from "react"; - -import { ValidationCase } from "../../../types/api-types"; - -import styles from "./ValidationSummary.module.css"; - -interface ValidationSummaryProps { - cases: ValidationCase[]; -} - -/** - * Displays summary statistics for a validation set in a compact inline format: - * N cases | splits: [badge] [badge] - */ -export const ValidationSummary: FC = ({ cases }) => { - const stats = useMemo(() => { - // Group cases by split with counts (only named splits) - const splitCounts = new Map(); - for (const c of cases) { - if (c.split) { - splitCounts.set(c.split, (splitCounts.get(c.split) ?? 0) + 1); - } - } - - // Sort splits alphabetically - const sortedSplits = Array.from(splitCounts.entries()).sort(([a], [b]) => - a.localeCompare(b) - ); - - return { - totalCount: cases.length, - splits: sortedSplits, - hasSplits: splitCounts.size > 0, - }; - }, [cases]); - - return ( -
- {stats.totalCount} cases - {stats.hasSplits && ( - <> - | - - Splits:{" "} - {stats.splits.map(([split, count], i) => ( - - {split} ({count}){i < stats.splits.length - 1 && ", "} - - ))} - - - )} -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/hooks/useTranscriptsByIds.ts b/src/inspect_scout/_view/www/src/app/validation/hooks/useTranscriptsByIds.ts deleted file mode 100644 index 19a20226c..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/hooks/useTranscriptsByIds.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { skipToken, useQuery } from "@tanstack/react-query"; -import { useMemo } from "react"; - -import { Column } from "../../../query"; -import { useApi } from "../../../state/store"; -import { TranscriptInfo } from "../../../types/api-types"; - -/** - * Hook to fetch transcripts by their IDs using an IN query. - * Returns a map of transcript_id -> TranscriptInfo for quick lookups. - * Also returns sourceIds for staleness detection - consumers should only - * trust lookups when the requested ID was in the sourceIds set. - */ -export const useTranscriptsByIds = ( - transcriptsDir: string | undefined, - ids: string[] -): { - data: Map | undefined; - sourceIds: Set | undefined; - loading: boolean; - error: Error | null; -} => { - const api = useApi(); - - // Create the IN filter condition - const filter = useMemo(() => { - if (ids.length === 0) return undefined; - return new Column("transcript_id").in(ids); - }, [ids]); - - // Stable query key based on sorted IDs - const queryKey = useMemo(() => { - const sortedIds = [...ids].sort(); - return ["transcriptsByIds", transcriptsDir, sortedIds]; - }, [transcriptsDir, ids]); - - const query = useQuery({ - queryKey, - queryFn: - !transcriptsDir || ids.length === 0 - ? skipToken - : async () => { - const response = await api.getTranscripts( - transcriptsDir, - filter, - undefined, - { limit: ids.length, cursor: null, direction: "forward" } - ); - return response.items; - }, - staleTime: 60 * 1000, - }); - - // Convert array to map for quick lookups - const transcriptMap = useMemo(() => { - if (!query.data) return undefined; - const map = new Map(); - for (const transcript of query.data) { - map.set(transcript.transcript_id, transcript); - } - return map; - }, [query.data]); - - // Track which IDs were used to build the current map - // This enables staleness detection in consumers - const sourceIds = useMemo(() => { - if (!query.data) return undefined; - return new Set(ids); - }, [query.data, ids]); - - return { - data: transcriptMap, - sourceIds, - loading: query.isLoading, - error: query.error, - }; -}; diff --git a/src/inspect_scout/_view/www/src/app/validation/utils.ts b/src/inspect_scout/_view/www/src/app/validation/utils.ts deleted file mode 100644 index de84dcb60..000000000 --- a/src/inspect_scout/_view/www/src/app/validation/utils.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { ValidationCase } from "../../types/api-types"; - -/** Valid file extensions for validation set files */ -export const VALIDATION_SET_EXTENSIONS = [ - ".csv", - ".json", - ".jsonl", - ".yml", - ".yaml", -]; - -/** - * Check if a filename has a valid validation set extension. - */ -export const hasValidationSetExtension = (name: string): boolean => { - const lower = name.toLowerCase(); - return VALIDATION_SET_EXTENSIONS.some((ext) => lower.endsWith(ext)); -}; - -/** - * Converts a validation case ID to a display string. - * Handles both single string IDs and composite (array) IDs. - */ -export const getIdText = (id: string | string[]): string => { - return Array.isArray(id) ? id.join(", ") : id; -}; - -/** - * Converts a validation case ID to a unique key for use in Maps/Sets. - * Uses "|" as separator for composite IDs. - */ -export const getCaseKey = (id: string | string[]): string => { - return Array.isArray(id) ? id.join("|") : id; -}; - -/** - * Extracts unique label keys from a list of validation cases. - * Returns sorted array of label key names. - */ -export const extractUniqueLabels = (cases: ValidationCase[]): string[] => { - const labelKeys = new Set(); - for (const c of cases) { - if (c.labels) { - for (const key of Object.keys(c.labels)) { - labelKeys.add(key); - } - } - } - return Array.from(labelKeys).sort(); -}; - -/** - * Extracts unique split values from a list of validation cases. - * Returns sorted array of non-empty split values. - */ -export const extractUniqueSplits = (cases: ValidationCase[]): string[] => { - const splitSet = new Set(); - for (const c of cases) { - if (c.split) { - splitSet.add(c.split); - } - } - return Array.from(splitSet).sort(); -}; - -/** - * Extracts the filename from a URI/path. - * @param uri - The full URI or path - * @param stripExtension - If true, removes common validation file extensions - */ -export const getFilenameFromUri = ( - uri: string, - stripExtension = false -): string => { - const filename = uri.split("/").pop() ?? uri; - if (stripExtension) { - return filename.replace(/\.(csv|json|jsonl|yaml|yml)$/i, ""); - } - return filename; -}; - -/** - * Extracts the directory portion of a URI (everything before the filename). - * @param uri - The full URI (e.g., "file:///path/to/file.csv") - * @returns The directory URI without trailing slash - */ -export const getDirFromUri = (uri: string): string => { - const parts = uri.split("/"); - parts.pop(); // Remove filename - return parts.join("/"); -}; - -/** - * Generates a new file URI in the same directory as the source with given name. - * @param sourceUri - The source file URI - * @param newName - The new filename (without extension) - * @returns New file URI with .csv extension - */ -export const generateNewSetUri = ( - sourceUri: string, - newName: string -): string => { - const dir = getDirFromUri(sourceUri); - return `${dir}/${newName}.csv`; -}; - -/** Characters that are not allowed in filenames */ -const INVALID_FILENAME_CHARS = /[/\\:*?"<>|]/; - -/** - * Validates a filename for invalid characters. - * @param name - The filename to validate (without extension) - * @returns Object with isValid and optional error message - */ -export const isValidFilename = ( - name: string -): { isValid: boolean; error?: string } => { - if (!name.trim()) { - return { isValid: false, error: "Name cannot be empty" }; - } - - if (INVALID_FILENAME_CHARS.test(name)) { - return { - isValid: false, - error: 'Name contains invalid characters: / \\ : * ? " < > |', - }; - } - - if (name.startsWith(".")) { - return { isValid: false, error: "Name cannot start with a dot" }; - } - - if (name.length > 255) { - return { isValid: false, error: "Name is too long (max 255 characters)" }; - } - - return { isValid: true }; -}; diff --git a/src/inspect_scout/_view/www/src/components/AnsiDisplay.module.css b/src/inspect_scout/_view/www/src/components/AnsiDisplay.module.css deleted file mode 100644 index d4692eddf..000000000 --- a/src/inspect_scout/_view/www/src/components/AnsiDisplay.module.css +++ /dev/null @@ -1,75 +0,0 @@ -.ansiDisplayContainer { - position: relative; - width: 100%; -} - -.ansiDisplay { - font-family: monospace; - white-space: pre-wrap; - line-height: normal; - --ansiBlack: #000000; - --ansiRed: #cd3131; - --ansiGreen: #00bc00; - --ansiYellow: #949800; - --ansiBlue: #0451a5; - --ansiMagenta: #bc05bc; - --ansiCyan: #0598bc; - --ansiWhite: #555555; - --ansiBrightBlack: #666666; - --ansiBrightRed: #cd3131; - --ansiBrightGreen: #14ce14; - --ansiBrightYellow: #b5ba00; - --ansiBrightBlue: #0451a5; - --ansiBrightMagenta: #bc05bc; - --ansiBrightCyan: #0598bc; - --ansiBrightWhite: #a5a5a5; -} - -.ansiDisplayRaw { - margin: 0; - white-space: pre-wrap; -} - -.ansiDisplayToggle { - position: absolute; - top: 0.25rem; - right: 0.25rem; - z-index: 10; - opacity: 0.7; - transition: opacity 0.2s; -} - -.ansiDisplayContainer:hover .ansiDisplayToggle { - opacity: 1; -} - -:global(.dark-mode) .ansiDisplay { - --ansiBlack: #000000; - --ansiRed: #cd3131; - --ansiGreen: #0dbc79; - --ansiYellow: #e5e510; - --ansiBlue: #2472c8; - --ansiMagenta: #bc3fbc; - --ansiCyan: #11a8cd; - --ansiWhite: #e5e5e5; - --ansiBrightBlack: #666666; - --ansiBrightRed: #f14c4c; - --ansiBrightGreen: #23d18b; - --ansiBrightYellow: #f5f543; - --ansiBrightBlue: #3b8eea; - --ansiBrightMagenta: #d670d6; - --ansiBrightCyan: #29b8db; - --ansiBrightWhite: #e5e5e5; -} - -@keyframes ansi-display-run-blink { - 50% { - opacity: 0; - } -} - -.ansiDisplayToggle { - padding: 0.1rem; - padding-left: 0.5rem; - background-color: var(--bs-body-bg); -} diff --git a/src/inspect_scout/_view/www/src/components/AnsiDisplay.tsx b/src/inspect_scout/_view/www/src/components/AnsiDisplay.tsx deleted file mode 100644 index 961e4582d..000000000 --- a/src/inspect_scout/_view/www/src/components/AnsiDisplay.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { ANSIColor, ANSIOutput, ANSIOutputRun, ANSIStyle } from "ansi-output"; -import clsx from "clsx"; -import { CSSProperties, FC, useState } from "react"; - -import styles from "./AnsiDisplay.module.css"; -import { ToolButton } from "./ToolButton"; - -interface ANSIDisplayProps { - output: string; - style?: CSSProperties; - className?: string[] | string; -} - -export const ANSIDisplay: FC = ({ - output, - style, - className, -}) => { - const [showRaw, setShowRaw] = useState(false); - const ansiOutput = new ANSIOutput(); - ansiOutput.processOutput(output); - - // Check if more than 80% of lines share the same background color - const getUniformBackgroundColor = (): string | undefined => { - const backgroundColorCounts = new Map(); - let totalLinesWithBackground = 0; - - // Count background colors across all lines - for (const line of ansiOutput.outputLines) { - let lineBackgroundColor: string | undefined = undefined; - - // Get the background color for this line (from any run that has one) - for (const run of line.outputRuns) { - if (run.format?.backgroundColor) { - lineBackgroundColor = run.format.backgroundColor; - break; - } - } - - if (lineBackgroundColor) { - totalLinesWithBackground++; - backgroundColorCounts.set( - lineBackgroundColor, - (backgroundColorCounts.get(lineBackgroundColor) || 0) + 1 - ); - } - } - - // Return undefined if no lines have backgrounds - if (totalLinesWithBackground === 0) { - return undefined; - } - - // Compute percentages for each background color - const backgroundColorPercentages = new Map(); - for (const [color, count] of backgroundColorCounts.entries()) { - backgroundColorPercentages.set(color, count / totalLinesWithBackground); - } - - // Find the color with the highest percentage - let dominantColor: string | undefined = undefined; - let maxPercentage = 0; - - for (const [color, percentage] of backgroundColorPercentages.entries()) { - if (percentage > maxPercentage) { - maxPercentage = percentage; - dominantColor = color; - } - } - - // Return the color if it appears in more than 80% of lines - return maxPercentage > 0.8 ? dominantColor : undefined; - }; - - const uniformBackgroundColor = getUniformBackgroundColor(); - const backgroundStyle = uniformBackgroundColor - ? computeForegroundBackgroundColor(kBackground, uniformBackgroundColor) - : {}; - - let firstOutput = false; - return ( -
- setShowRaw(!showRaw)} - title={showRaw ? "Show rendered output" : "Show raw output"} - /> - {showRaw ? ( -
-          {output}
-        
- ) : ( -
- {ansiOutput.outputLines.map((line, index) => { - // TODO: lint react-hooks/immutability - the mutation of firstOutput is funky. Render functions are supposed to be pure. - // eslint-disable-next-line react-hooks/immutability - firstOutput = firstOutput || !!line.outputRuns.length; - return ( -
- {!line.outputRuns.length ? ( - firstOutput ? ( -
- ) : null - ) : ( - line.outputRuns.map((outputRun) => ( - - )) - )} -
- ); - })} -
- )} -
- ); -}; - -const kForeground = 0; -const kBackground = 1; - -interface OutputRunProps { - run: ANSIOutputRun; -} - -const OutputRun: FC = ({ run }) => { - // Render. - return {run.text}; -}; - -const computeCSSProperties = (outputRun: ANSIOutputRun) => { - return !outputRun.format - ? {} - : { - ...computeStyles(outputRun.format.styles || []), - ...computeForegroundBackgroundColor( - kForeground, - outputRun.format.foregroundColor - ), - ...computeForegroundBackgroundColor( - kBackground, - outputRun.format.backgroundColor - ), - }; -}; - -const computeStyles = (styles: ANSIStyle[]) => { - let cssProperties = {}; - if (styles) { - styles.forEach((style) => { - switch (style) { - // Bold. - case ANSIStyle.Bold: - cssProperties = { ...cssProperties, ...{ fontWeight: "bold" } }; - break; - - // Dim. - case ANSIStyle.Dim: - cssProperties = { ...cssProperties, ...{ fontWeight: "lighter" } }; - break; - - // Italic. - case ANSIStyle.Italic: - cssProperties = { ...cssProperties, ...{ fontStyle: "italic" } }; - break; - - // Underlined. - case ANSIStyle.Underlined: - cssProperties = { - ...cssProperties, - ...{ - textDecorationLine: "underline", - textDecorationStyle: "solid", - }, - }; - break; - - // Slow blink. - case ANSIStyle.SlowBlink: - cssProperties = { - ...cssProperties, - ...{ animation: "ansi-display-run-blink 1s linear infinite" }, - }; - break; - - // Rapid blink. - case ANSIStyle.RapidBlink: - cssProperties = { - ...cssProperties, - ...{ animation: "ansi-display-run-blink 0.5s linear infinite" }, - }; - break; - - // Hidden. - case ANSIStyle.Hidden: - cssProperties = { ...cssProperties, ...{ visibility: "hidden" } }; - break; - - // CrossedOut. - case ANSIStyle.CrossedOut: - cssProperties = { - ...cssProperties, - ...{ - textDecorationLine: "line-through", - textDecorationStyle: "solid", - }, - }; - break; - - // TODO Fraktur - - // DoubleUnderlined. - case ANSIStyle.DoubleUnderlined: - cssProperties = { - ...cssProperties, - ...{ - textDecorationLine: "underline", - textDecorationStyle: "double", - }, - }; - break; - - // TODO Framed - // TODO Encircled - // TODO Overlined - // TODO Superscript - // TODO Subscript - } - }); - } - - return cssProperties; -}; - -const computeForegroundBackgroundColor = ( - colorType: number, - color?: string -) => { - switch (color) { - // Undefined. - case undefined: - return {}; - - // One of the standard colors. - case ANSIColor.Black: - case ANSIColor.Red: - case ANSIColor.Green: - case ANSIColor.Yellow: - case ANSIColor.Blue: - case ANSIColor.Magenta: - case ANSIColor.Cyan: - case ANSIColor.White: - case ANSIColor.BrightBlack: - case ANSIColor.BrightRed: - case ANSIColor.BrightGreen: - case ANSIColor.BrightYellow: - case ANSIColor.BrightBlue: - case ANSIColor.BrightMagenta: - case ANSIColor.BrightCyan: - case ANSIColor.BrightWhite: - if (colorType === kForeground) { - return { color: `var(--${color})` }; - } else { - return { background: `var(--${color})` }; - } - - // TODO@softwarenerd - This isn't hooked up. - default: - if (colorType === kForeground) { - return { color: color }; - } else { - return { background: color }; - } - } -}; diff --git a/src/inspect_scout/_view/www/src/components/AsciinemaPlayer.tsx b/src/inspect_scout/_view/www/src/components/AsciinemaPlayer.tsx deleted file mode 100644 index b2f8136e6..000000000 --- a/src/inspect_scout/_view/www/src/components/AsciinemaPlayer.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as AsciicinemaPlayerJS from "asciinema-player"; -import "asciinema-player/dist/bundle/asciinema-player.css"; -import { CSSProperties, FC, useEffect, useRef } from "react"; - -interface AsciinemaPlayerProps { - id?: string; - inputUrl: string; - outputUrl: string; - timingUrl: string; - rows?: number; - cols?: number; - fit?: string; - style?: CSSProperties; - speed?: number; - autoPlay?: boolean; - loop?: boolean; - theme?: string; - idleTimeLimit?: number; - className?: string; -} - -export const AsciinemaPlayer: FC = ({ - id, - rows, - cols, - inputUrl, - outputUrl, - timingUrl, - fit, - speed, - autoPlay, - loop, - theme, - idleTimeLimit = 2, - style, -}) => { - const playerContainerRef = useRef(null); - - useEffect(() => { - if (!playerContainerRef.current) return; - - const player = AsciicinemaPlayerJS.create( - { - url: [timingUrl, outputUrl, inputUrl], - parser: "typescript", - }, - playerContainerRef.current, - { - rows, - cols, - autoPlay, - loop, - theme, - speed, - idleTimeLimit, - fit, - } - ); - - player.play(); - - return () => { - player.dispose(); - }; - }, [ - timingUrl, - outputUrl, - inputUrl, - rows, - cols, - autoPlay, - loop, - theme, - speed, - idleTimeLimit, - fit, - ]); - - return ( -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/components/AutocompleteInput.module.css b/src/inspect_scout/_view/www/src/components/AutocompleteInput.module.css deleted file mode 100644 index e4c9f818f..000000000 --- a/src/inspect_scout/_view/www/src/components/AutocompleteInput.module.css +++ /dev/null @@ -1,76 +0,0 @@ -.container { - position: relative; - width: 100%; -} - -.input { - width: 100%; - font-size: 12px; - padding: 4px 6px; - border-radius: 4px; - border: 1px solid var(--bs-border-color); - background: var(--bs-body-bg); - color: var(--bs-body-color); -} - -.input:disabled { - background: var(--bs-light); - color: var(--bs-secondary); -} - -.inputWithToggle { - padding-right: 24px; -} - -.toggleButton { - position: absolute; - right: 2px; - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - padding: 2px 4px; - cursor: pointer; - color: var(--bs-body-color); - display: flex; - align-items: center; - justify-content: center; - font-size: 10px; -} - -.toggleButton:hover:not(:disabled) { - color: var(--bs-body-color); -} - -.toggleButton:disabled { - cursor: not-allowed; - opacity: 0.5; -} - -.suggestionsList { - margin: 0; - padding: 0; - list-style: none; - background: var(--bs-body-bg); - border: 1px solid var(--bs-border-color); - border-radius: 4px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - max-height: 200px; - overflow-y: auto; - z-index: 10000; -} - -.suggestionItem { - padding: 6px 8px; - font-size: 12px; - cursor: pointer; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.suggestionItem:hover, -.suggestionItem.highlighted { - background: var(--bs-primary); - color: white; -} diff --git a/src/inspect_scout/_view/www/src/components/AutocompleteInput.tsx b/src/inspect_scout/_view/www/src/components/AutocompleteInput.tsx deleted file mode 100644 index 4885e205e..000000000 --- a/src/inspect_scout/_view/www/src/components/AutocompleteInput.tsx +++ /dev/null @@ -1,353 +0,0 @@ -import clsx from "clsx"; -import { - ChangeEvent, - FC, - KeyboardEvent, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { createPortal } from "react-dom"; - -import styles from "./AutocompleteInput.module.css"; - -export interface AutocompleteInputProps { - id: string; - value: string; - onChange: (value: string) => void; - onCommit?: () => void; - onCancel?: () => void; - disabled?: boolean; - placeholder?: string; - suggestions: Array; - autoFocus?: boolean; - maxSuggestions?: number; - charactersBeforeSuggesting?: number; - maxSuggestionWidth?: number; - className?: string; - /** When true, shows a dropdown toggle icon to browse all options */ - allowBrowse?: boolean; -} - -export const AutocompleteInput: FC = ({ - id, - value, - onChange, - onCommit, - onCancel, - disabled, - suggestions, - placeholder = "Filter", - maxSuggestions = 10, - charactersBeforeSuggesting = 1, - maxSuggestionWidth = 300, - autoFocus, - className, - allowBrowse = false, -}) => { - const inputRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - // Start with no selection (-1) so Enter submits the typed value, not a suggestion - const [highlightedIndex, setHighlightedIndex] = useState(-1); - // Track whether dropdown was opened via browse icon (shows all options) - const [isBrowseMode, setIsBrowseMode] = useState(false); - const [dropdownPosition, setDropdownPosition] = useState<{ - top: number; - left: number; - width: number; - } | null>(null); - const listRef = useRef(null); - const containerRef = useRef(null); - // Track whether user has typed in the input (to avoid showing suggestions on initial focus) - const hasTypedRef = useRef(false); - - // Filter suggestions based on current input (or show all in browse mode) - const filteredSuggestions = useMemo(() => { - // In browse mode, show all non-null suggestions (no limit) - if (isBrowseMode) { - return suggestions.filter((s) => s !== null); - } - // Normal mode: require minimum characters before showing suggestions - if (value.length < charactersBeforeSuggesting) { - return []; - } - const lowerValue = value.toLowerCase(); - return suggestions - .filter((s) => { - if (s === null) return false; - const strValue = String(s).toLowerCase(); - // Exclude exact matches - no point suggesting what's already there - if (strValue === lowerValue) return false; - return strValue.includes(lowerValue); - }) - .slice(0, maxSuggestions); - }, [ - suggestions, - value, - maxSuggestions, - charactersBeforeSuggesting, - isBrowseMode, - ]); - - // Determine if dropdown should be shown - const showDropdown = isOpen && filteredSuggestions.length > 0; - - // Update dropdown position when showing - useEffect(() => { - if (showDropdown && containerRef.current) { - const rect = containerRef.current.getBoundingClientRect(); - // Use viewport coordinates since we're using position: fixed - setDropdownPosition({ - top: rect.bottom + 2, - left: rect.left, - width: rect.width, - }); - } - }, [showDropdown]); - - // Reset highlight when suggestions change (no selection by default) - useEffect(() => { - // TODO: lint react-hooks/set-state-in-effect - consider if fixing this violation makes sense - // eslint-disable-next-line react-hooks/set-state-in-effect - setHighlightedIndex(-1); - }, [filteredSuggestions]); - - // Select all text when autoFocus is enabled (show start of text, not end) - useEffect(() => { - if (autoFocus && inputRef.current) { - inputRef.current.select(); - inputRef.current.scrollLeft = 0; - } - }, [autoFocus]); - - // Show dropdown when input is focused and has suggestions (only if user has typed) - const handleFocus = useCallback(() => { - if (hasTypedRef.current && filteredSuggestions.length > 0) { - setIsOpen(true); - } - }, [filteredSuggestions.length]); - - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if ( - containerRef.current && - !containerRef.current.contains(e.target as Node) - ) { - setIsOpen(false); - } - }; - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - - const handleInputChange = useCallback( - (e: ChangeEvent) => { - hasTypedRef.current = true; - setIsBrowseMode(false); // Exit browse mode when user types - onChange(e.target.value); - setIsOpen(true); - }, - [onChange] - ); - - const handleToggleBrowse = useCallback(() => { - if (isOpen && isBrowseMode) { - // If already open in browse mode, close it - setIsOpen(false); - setIsBrowseMode(false); - } else { - // Open in browse mode - setIsBrowseMode(true); - setIsOpen(true); - } - inputRef.current?.focus(); - }, [isOpen, isBrowseMode]); - - const selectSuggestion = useCallback( - (suggestion: string | number | boolean | null) => { - onChange(String(suggestion ?? "")); - setIsOpen(false); - setIsBrowseMode(false); - inputRef.current?.focus(); - }, - [onChange, inputRef] - ); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (!showDropdown) { - // Pass through to parent handlers when dropdown is not shown - if (e.key === "Escape") { - e.preventDefault(); - e.stopPropagation(); - onCancel?.(); - } else if (e.key === "Enter") { - e.preventDefault(); - e.stopPropagation(); - onCommit?.(); - } - return; - } - - switch (e.key) { - case "ArrowDown": - e.preventDefault(); - e.stopPropagation(); - setHighlightedIndex((prev) => - Math.min(prev + 1, filteredSuggestions.length - 1) - ); - break; - - case "ArrowUp": - e.preventDefault(); - e.stopPropagation(); - setHighlightedIndex((prev) => Math.max(prev - 1, -1)); - break; - - case "Tab": - // Tab completes the highlighted suggestion (only if one is selected) - if ( - highlightedIndex >= 0 && - filteredSuggestions[highlightedIndex] !== undefined - ) { - e.preventDefault(); - selectSuggestion(filteredSuggestions[highlightedIndex]); - } - break; - - case "Enter": - e.preventDefault(); - e.stopPropagation(); - // Only use suggestion if one is highlighted, otherwise submit typed value - if ( - highlightedIndex >= 0 && - filteredSuggestions[highlightedIndex] !== undefined - ) { - selectSuggestion(filteredSuggestions[highlightedIndex]); - } - // Always commit (either selected suggestion or typed value) - onCommit?.(); - break; - - case "Escape": - e.preventDefault(); - e.stopPropagation(); - // First escape closes dropdown, second cancels - setIsOpen(false); - break; - } - }, - [ - showDropdown, - filteredSuggestions, - highlightedIndex, - selectSuggestion, - onCommit, - onCancel, - ] - ); - - // Scroll highlighted item into view - useEffect(() => { - if (listRef.current && showDropdown && highlightedIndex >= 0) { - const highlighted = listRef.current.children[ - highlightedIndex - ] as HTMLElement; - highlighted?.scrollIntoView({ block: "nearest" }); - } - }, [highlightedIndex, showDropdown]); - - return ( -
- = 0 - ? `${id}-option-${highlightedIndex}` - : undefined - } - autoFocus={autoFocus} - /> - {allowBrowse && suggestions.length > 0 && ( - - )} - - {showDropdown && - dropdownPosition && - createPortal( -
    - {filteredSuggestions.map((suggestion, index) => ( -
  • { - // Prevent mousedown from triggering outside click handlers (including PopOver) - e.preventDefault(); - e.stopPropagation(); - selectSuggestion(suggestion); - }} - onMouseEnter={() => setHighlightedIndex(index)} - > - {String(suggestion ?? "(null)")} -
  • - ))} -
, - document.body - )} -
- ); -}; diff --git a/src/inspect_scout/_view/www/src/components/Card.css b/src/inspect_scout/_view/www/src/components/Card.css deleted file mode 100644 index 57ead9d36..000000000 --- a/src/inspect_scout/_view/www/src/components/Card.css +++ /dev/null @@ -1,71 +0,0 @@ -.card-header-container:not(.card-header-modern) { - display: grid; - grid-template-columns: max-content auto; - column-gap: 0rem; - padding: 0.5rem 0.5rem 0.5rem 0.5rem; - font-size: var(--inspect-font-size-small); - font-weight: 600; - border-bottom: solid 1px var(--bs-light-border-subtle); -} - -.card-header-container.card-header-modern { - display: grid; - grid-template-columns: max-content auto; - column-gap: 0rem; - font-size: var(--inspect-font-size-small); - color: var(--bs-secondary); - padding: 0.5rem 0.5rem 0rem 0.5rem; -} - -.card-header-icon:not(.card-header-icon-empty) { - padding-right: 0.2rem; -} - -.card-body { - background-color: var(--bs-body-bg); - padding: 0.5rem !important; -} - -.card { - background-color: var(--bs-light-bg-subtle); - border: solid 1px var(--bs-light-border-subtle); - border-radius: var(--bs-border-radius); - overflow: hidden; -} - -.card-collaping-header { - border-bottom: none; -} - -.card-collapsing-header-container { - justify-content: space-between; - align-items: center; -} - -.card-collapsing-header-icon { - flex: 0 0 content; - padding-right: 0.5rem; -} - -.card-collapsing-header-contents { - color: var(--body-color); - opacity: 0.8; - flex: 1 1 auto; - font-size: var(--inspect-font-size-smaller); - padding-right: 0; - padding-left: 0; - transition: opacity 0.2s ease-out; - display: flex; - justify-content: space-between; -} - -.card-collapsing-header-toggle { - flex: 0 1 1em; - text-align: right; - padding: 0 0.5em 0.1em 0.5em; - font-size: var(--inspect-font-size-smaller); -} - -.card-body.card-no-padding { - padding: 0; -} diff --git a/src/inspect_scout/_view/www/src/components/Card.tsx b/src/inspect_scout/_view/www/src/components/Card.tsx deleted file mode 100644 index 5c5cf2a89..000000000 --- a/src/inspect_scout/_view/www/src/components/Card.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import clsx from "clsx"; -import { FC, ReactNode } from "react"; - -import { ApplicationIcons } from "./icons"; -import "./Card.css"; - -interface CardHeaderProps { - id?: string; - icon?: string; - label?: string; - type?: "default" | "modern"; - className?: string; - children?: ReactNode; -} - -interface CardBodyProps { - id?: string; - children?: ReactNode; - className?: string | string[]; - padded?: boolean; -} - -interface CardProps { - id?: string; - children?: ReactNode; - className?: string | string[]; -} - -interface CardCollapsingHeaderProps { - id: string; - icon: string; - label: string; - cardBodyId: string; - children?: ReactNode; -} - -export const CardHeader: FC = ({ - id, - icon, - type, - label, - className, - children, -}) => { - return ( -
- {icon ? ( - - ) : ( - - )} - {label ? label : ""} {children} -
- ); -}; - -export const CardBody: FC = ({ - id, - children, - className, - padded = true, -}) => { - return ( -
- {children} -
- ); -}; - -export const Card: FC = ({ id, children, className }) => { - return ( -
- {children} -
- ); -}; - -export const CardCollapsingHeader: FC = ({ - id, - icon, - label, - cardBodyId, - children, -}) => { - return ( - - - - ); -}; diff --git a/src/inspect_scout/_view/www/src/components/ConfirmationDialog.module.css b/src/inspect_scout/_view/www/src/components/ConfirmationDialog.module.css deleted file mode 100644 index 0bd3201ab..000000000 --- a/src/inspect_scout/_view/www/src/components/ConfirmationDialog.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.content { - display: flex; - flex-direction: column; - gap: 12px; -} - -.content p { - margin: 0; -} - -.warning { - color: var(--vscode-errorForeground); - font-size: 0.9rem; -} diff --git a/src/inspect_scout/_view/www/src/components/ConfirmationDialog.tsx b/src/inspect_scout/_view/www/src/components/ConfirmationDialog.tsx deleted file mode 100644 index f31234ae1..000000000 --- a/src/inspect_scout/_view/www/src/components/ConfirmationDialog.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { VscodeButton } from "@vscode-elements/react-elements"; -import { FC, ReactNode } from "react"; - -import styles from "./ConfirmationDialog.module.css"; -import { Modal } from "./Modal"; - -interface ConfirmationDialogProps { - show: boolean; - onHide: () => void; - onConfirm: () => void; - title: string; - message: ReactNode; - confirmLabel?: string; - cancelLabel?: string; - confirmingLabel?: string; - isConfirming?: boolean; - warning?: string; -} - -export const ConfirmationDialog: FC = ({ - show, - onHide, - onConfirm, - title, - message, - confirmLabel = "Confirm", - cancelLabel = "Cancel", - confirmingLabel = "Confirming...", - isConfirming = false, - warning, -}) => { - return ( - - - {cancelLabel} - - - {isConfirming ? confirmingLabel : confirmLabel} - - - } - > -
-

{message}

- {warning &&

{warning}

} -
-
- ); -}; diff --git a/src/inspect_scout/_view/www/src/components/CopyButton.module.css b/src/inspect_scout/_view/www/src/components/CopyButton.module.css deleted file mode 100644 index e9ac443f3..000000000 --- a/src/inspect_scout/_view/www/src/components/CopyButton.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.copyButton { - border: none; - background-color: inherit; - opacity: 0.5; - padding-top: 0; - transition: opacity 0.2s; -} - -.copyButton:hover { - opacity: 0.75; -} diff --git a/src/inspect_scout/_view/www/src/components/CopyButton.tsx b/src/inspect_scout/_view/www/src/components/CopyButton.tsx deleted file mode 100644 index 4a6b8a807..000000000 --- a/src/inspect_scout/_view/www/src/components/CopyButton.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import clsx from "clsx"; -import { JSX, useState } from "react"; - -import styles from "./CopyButton.module.css"; -import { ApplicationIcons } from "./icons"; - -interface CopyButtonProps { - icon?: string; - title?: string; - value: string; - onCopySuccess?: () => void; - onCopyError?: (error: Error) => void; - className?: string; - ariaLabel?: string; -} - -export const CopyButton = ({ - icon = ApplicationIcons.copy, - title, - value, - onCopySuccess, - onCopyError, - className = "", - ariaLabel = "Copy to clipboard", -}: CopyButtonProps): JSX.Element => { - const [isCopied, setIsCopied] = useState(false); - - const handleClick = async (): Promise => { - try { - await navigator.clipboard.writeText(value); - setIsCopied(true); - onCopySuccess?.(); - - // Reset copy state after delay - setTimeout(() => { - setIsCopied(false); - }, 1250); - } catch (error) { - onCopyError?.( - error instanceof Error ? error : new Error("Failed to copy") - ); - } - }; - - return ( -